diff --git a/config-example/bot.yml b/config-example/bot.yml index d832fa0b..eae5ca33 100644 --- a/config-example/bot.yml +++ b/config-example/bot.yml @@ -24,14 +24,6 @@ debug: true # true: enable logging to console # false: disable logging -log_json: false -# true: logs will be in JOSN format -# false: logs will be in plain text - -interactive_components: false -# true: enables users to create and interact with chat platforms interactive components (e.g. Slack message attachments) -# false (defualt): disables interactive components for all supported chat platform - ## heroku deploys require an injected listener port; only works for Slack Apps # slack_listener_port: ${PORT} diff --git a/core/configure_test.go b/core/configure_test.go index 3000477f..9450a4e5 100644 --- a/core/configure_test.go +++ b/core/configure_test.go @@ -115,26 +115,25 @@ func Test_configureChatApplication(t *testing.T) { validateRemoteSetup(testBotTelegramBadToken) tests := []struct { - name string - args args - shouldRunChat bool - shouldRunInteractiveComponents bool + name string + args args + shouldRunChat bool }{ - {"Fail", args{bot: testBot}, false, false}, - {"Fail - no chat_application not set", args{bot: testBotNoChat}, false, false}, - {"Fail - Invalid value for chat_application", args{bot: testBotInvalidChat}, false, false}, - {"Bad Name", args{bot: testBotBadName}, false, false}, - {"Slack - no token", args{bot: testBotSlackNoToken}, false, false}, - {"Slack - bad token", args{bot: testBotSlackBadToken}, false, false}, - {"Slack - bad signing secret", args{bot: testBotSlackBadSigningSecret}, false, false}, - {"Slack", args{bot: testBotSlack}, true, false}, - {"Discord - no token", args{bot: testBotDiscordNoToken}, false, false}, - {"Discord - bad token", args{bot: testBotDiscordBadToken}, false, false}, - {"Discord w/ server id", args{bot: testBotDiscordServerID}, true, false}, - {"Discord w/ bad server id", args{bot: testBotDiscordBadServerID}, false, false}, - {"Telegram", args{bot: testBotTelegram}, true, false}, - {"Telegram - no token", args{bot: testBotTelegramNoToken}, false, false}, - {"Telegram - bad token", args{bot: testBotTelegramBadToken}, false, false}, + {"Fail", args{bot: testBot}, false}, + {"Fail - no chat_application not set", args{bot: testBotNoChat}, false}, + {"Fail - Invalid value for chat_application", args{bot: testBotInvalidChat}, false}, + {"Bad Name", args{bot: testBotBadName}, false}, + {"Slack - no token", args{bot: testBotSlackNoToken}, false}, + {"Slack - bad token", args{bot: testBotSlackBadToken}, false}, + {"Slack - bad signing secret", args{bot: testBotSlackBadSigningSecret}, false}, + {"Slack", args{bot: testBotSlack}, true}, + {"Discord - no token", args{bot: testBotDiscordNoToken}, false}, + {"Discord - bad token", args{bot: testBotDiscordBadToken}, false}, + {"Discord w/ server id", args{bot: testBotDiscordServerID}, true}, + {"Discord w/ bad server id", args{bot: testBotDiscordBadServerID}, false}, + {"Telegram", args{bot: testBotTelegram}, true}, + {"Telegram - no token", args{bot: testBotTelegramNoToken}, false}, + {"Telegram - bad token", args{bot: testBotTelegramBadToken}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -142,10 +141,6 @@ func Test_configureChatApplication(t *testing.T) { if tt.shouldRunChat != tt.args.bot.RunChat { t.Errorf("configureChatApplication() wanted RunChat set to %v, but got %v", tt.shouldRunChat, tt.args.bot.RunChat) } - - if tt.shouldRunInteractiveComponents != tt.args.bot.InteractiveComponents { - t.Errorf("configureChatApplication() wanted InteractiveComponents set to %v, but got %v", tt.shouldRunInteractiveComponents, tt.args.bot.InteractiveComponents) - } }) } } @@ -157,7 +152,6 @@ func Test_setSlackListenerPort(t *testing.T) { baseBot := func() *models.Bot { bot := new(models.Bot) bot.CLI = true - bot.InteractiveComponents = true bot.ChatApplication = "slack" bot.SlackToken = "${TEST_SLACK_TOKEN}" bot.SlackInteractionsCallbackPath = "${TEST_SLACK_INTERACTIONS_CALLBACK_PATH}" diff --git a/core/outputs.go b/core/outputs.go index f2bf2447..81c39608 100644 --- a/core/outputs.go +++ b/core/outputs.go @@ -49,10 +49,6 @@ func Outputs(outputMsgs <-chan models.Message, hitRule <-chan models.Rule, bot * } if service == models.MsgServiceChat { - if bot.InteractiveComponents { - remoteSlack.InteractiveComponents(nil, &message, rule, bot) - } - remoteSlack.Reaction(message, rule, bot) } diff --git a/core/remotes.go b/core/remotes.go index e2b51b79..bb999017 100644 --- a/core/remotes.go +++ b/core/remotes.go @@ -64,7 +64,6 @@ func Remotes(inputMsgs chan<- models.Message, rules map[string]models.Rule, bot } // Read messages from Slack go remoteSlack.Read(inputMsgs, rules, bot) - go remoteSlack.InteractiveComponents(inputMsgs, nil, rules[""], bot) // Setup remote to use the Telegram client to read from Telegram case "telegram": remoteTelegram := &telegram.Client{ diff --git a/models/bot.go b/models/bot.go index a444db59..288c8e5b 100644 --- a/models/bot.go +++ b/models/bot.go @@ -37,7 +37,6 @@ type Bot struct { Scheduler bool `mapstructure:"scheduler,omitempty"` ChatApplication string `mapstructure:"chat_application" binding:"required"` Debug bool `mapstructure:"debug,omitempty"` - InteractiveComponents bool `mapstructure:"interactive_components,omitempty"` Metrics bool `mapstructure:"metrics,omitempty"` CustomHelpText string `mapstructure:"custom_help_text,omitempty"` DisableNoMatchHelp bool `mapstructure:"disable_no_match_help,omitempty"` diff --git a/remote/cli/remote.go b/remote/cli/remote.go index 9cfeb83f..d635e68b 100644 --- a/remote/cli/remote.go +++ b/remote/cli/remote.go @@ -94,8 +94,3 @@ func (c *Client) Send(message models.Message, bot *models.Bot) { fmt.Fprint(w, user+"> ") w.Flush() } - -// InteractiveComponents implementation to satisfy remote interface. -func (c *Client) InteractiveComponents(inputMsgs chan<- models.Message, message *models.Message, rule models.Rule, bot *models.Bot) { - // not implemented for CLI -} diff --git a/remote/discord/remote.go b/remote/discord/remote.go index 1e4b9bf6..cf379492 100644 --- a/remote/discord/remote.go +++ b/remote/discord/remote.go @@ -175,11 +175,6 @@ func (c *Client) Send(message models.Message, bot *models.Bot) { } } -// InteractiveComponents implementation to satisfy remote interface. -func (c *Client) InteractiveComponents(inputMsgs chan<- models.Message, message *models.Message, rule models.Rule, bot *models.Bot) { - // not implemented for Discord -} - // This function will be called (due to AddHandler above) every time a new // message is created on any channel that the authenticated bot has access to. func handleDiscordMessage(bot *models.Bot, inputMsgs chan<- models.Message) any { diff --git a/remote/gchat/remote.go b/remote/gchat/remote.go index 3ae72449..532b56ba 100644 --- a/remote/gchat/remote.go +++ b/remote/gchat/remote.go @@ -109,11 +109,6 @@ func (c *Client) Send(message models.Message, bot *models.Bot) { } } -// InteractiveComponents implementation to satisfy remote interface. -func (c *Client) InteractiveComponents(inputMsgs chan<- models.Message, message *models.Message, rule models.Rule, bot *models.Bot) { - // TODO: add support for InteractiveComponents with Google Chat Cards -} - // Reaction implementation to satisfy remote interface. func (c *Client) Reaction(message models.Message, rule models.Rule, bot *models.Bot) { // Not implemented for Google Chat diff --git a/remote/remote.go b/remote/remote.go index 0eaf2024..81942917 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -25,8 +25,6 @@ type Remote interface { Send(message models.Message, bot *models.Bot) - InteractiveComponents(inputMsgs chan<- models.Message, message *models.Message, rule models.Rule, bot *models.Bot) - Name() string } @@ -45,11 +43,6 @@ func Send(c context.Context, message models.Message, bot *models.Bot) { FromContext(c).Send(message, bot) } -// InteractiveComponents enables the bot to listen to Interactive Components coming from a remote. -func InteractiveComponents(c context.Context, inputMsgs chan<- models.Message, message *models.Message, rule models.Rule, bot *models.Bot) { - FromContext(c).InteractiveComponents(inputMsgs, message, rule, bot) -} - // Name returns the name of the remote. func Name(c context.Context) string { return FromContext(c).Name() diff --git a/remote/scheduler/remote.go b/remote/scheduler/remote.go index 095ce03c..f332605e 100644 --- a/remote/scheduler/remote.go +++ b/remote/scheduler/remote.go @@ -127,11 +127,6 @@ func (c *Client) Send(message models.Message, bot *models.Bot) { // not implemented for Scheduler } -// InteractiveComponents implementation to satisfy remote interface. -func (c *Client) InteractiveComponents(inputMsgs chan<- models.Message, message *models.Message, rule models.Rule, bot *models.Bot) { - // not implemented for Scheduler -} - // Process the Cron jobs. func processJobs(jobs []*cron.Cron) { // Create wait group for cron jobs and execute them diff --git a/remote/slack/helper.go b/remote/slack/helper.go index 459bfea8..df133976 100644 --- a/remote/slack/helper.go +++ b/remote/slack/helper.go @@ -19,7 +19,6 @@ import ( "github.com/slack-go/slack/socketmode" "github.com/target/flottbot/models" - "github.com/target/flottbot/utils" ) /* @@ -28,71 +27,6 @@ Slack helper functions (anything that uses the 'slack-go/slack' package) ====================================================================== */ -// constructInteractiveComponentMessage creates a message specifically for a matched rule from the Interactive Components server. -func constructInteractiveComponentMessage(callback slack.AttachmentActionCallback, bot *models.Bot) models.Message { - text := "" - - if len(callback.ActionCallback.AttachmentActions) > 0 { - for _, action := range callback.ActionCallback.AttachmentActions { - if action.Value != "" { - text = fmt.Sprintf("<@%s> %s", bot.ID, action.Value) - break - } - } - } - - message := models.NewMessage() - - messageType, err := getMessageType(callback.Channel.ID) - if err != nil { - log.Error().Msg(err.Error()) - } - - userNames := strings.Split(callback.User.Name, ".") - user := &slack.User{ - ID: callback.User.ID, - TeamID: callback.User.TeamID, - Name: callback.User.Name, - Color: callback.User.Color, - RealName: callback.User.RealName, - TZ: callback.User.TZ, - TZLabel: callback.User.TZLabel, - TZOffset: callback.User.TZOffset, - Profile: slack.UserProfile{ - FirstName: userNames[0], - LastName: userNames[len(userNames)-1], - RealNameNormalized: callback.User.Profile.RealNameNormalized, - DisplayName: callback.User.Profile.DisplayName, - DisplayNameNormalized: callback.User.Profile.DisplayName, - Email: callback.User.Profile.Email, - Skype: callback.User.Profile.Skype, - Phone: callback.User.Profile.Phone, - Title: callback.User.Profile.Title, - StatusText: callback.User.Profile.StatusText, - StatusEmoji: callback.User.Profile.StatusEmoji, - Team: callback.User.Profile.Team, - }, - } - channel := callback.Channel.Name - - if callback.Channel.IsPrivate { - channel = callback.Channel.ID - } - - msgType, err := getMessageType(callback.Channel.ID) - if err != nil { - log.Error().Msg(err.Error()) - } - - if msgType == models.MsgTypePrivateChannel { - channel = callback.Channel.ID - } - - contents, mentioned := removeBotMention(text, bot.ID) - - return populateMessage(message, messageType, channel, contents, callback.MessageTs, callback.MessageTs, "", mentioned, user, bot) -} - // getEventsAPIHealthHandler creates and returns the handler for health checks on the Slack Events API reader. func getEventsAPIHealthHandler(bot *models.Bot) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { @@ -284,101 +218,6 @@ func getEventsAPIEventHandler(api *slack.Client, signingSecret string, inputMsgs } } -// getInteractiveComponentHealthHandler creates and returns the handler for health checks on the Interactive Component server. -func getInteractiveComponentHealthHandler(bot *models.Bot) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - log.Error().Msgf("received invalid method: %s", r.Method) - w.WriteHeader(http.StatusMethodNotAllowed) - - return - } - - log.Debug().Msg("bot interaction health endpoint hit") - - w.WriteHeader(http.StatusOK) - - _, err := w.Write([]byte("OK")) - if err != nil { - log.Error().Msgf("failed to handle interactive component: %v", err) - } - } -} - -// getInteractiveComponentRuleHandler creates and returns the handler for processing and sending out messages from the Interactive Component server. -func getInteractiveComponentRuleHandler(inputMsgs chan<- models.Message, rule models.Rule, bot *models.Bot) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - log.Error().Msgf("received invalid method: %s", r.Method) - - w.WriteHeader(http.StatusMethodNotAllowed) - w.Header().Set("Content-Type", "text/plain") - - _, err := w.Write([]byte("Oops! I encountered an unexpected HTTP verb")) - if err != nil { - log.Error().Msgf("failed to send response for interactive component handler: %v", err) - } - - return - } - - buff, err := io.ReadAll(r.Body) - if err != nil { - log.Error().Msgf("failed to read request body: %v", err) - } - - contents, err := sanitizeContents(buff) - if err != nil { - log.Error().Msgf("failed to sanitize content: %v", err) - } - - var callback slack.AttachmentActionCallback - if err := json.Unmarshal([]byte(contents), &callback); err != nil { - log.Error().Msgf("failed to decode callback json %#q: %v", contents, err) - - w.WriteHeader(http.StatusInternalServerError) - w.Header().Set("Content-Type", "text/plain") - - _, err := w.Write([]byte("Oops! Looks like I failed to decode some JSON in the backend. Please contact admins for more info!")) - if err != nil { - log.Error().Msgf("failed to send response for error during unmarshal process: %v", err) - } - - return - } - - // Only accept message from slack with valid token - if callback.Token != bot.SlackSigningSecret { - log.Error().Msg("invalid 'slack_signing_secret'") - - w.WriteHeader(http.StatusUnauthorized) - w.Header().Set("Content-Type", "text/plain") - - _, err := w.Write([]byte("Sorry, but I didn't recognize your signing secret! Perhaps check if it's a valid secret.")) - if err != nil { - log.Error().Msg("failed to send response for validating secret.") - } - - return - } - - // Construct and send out message - message := constructInteractiveComponentMessage(callback, bot) - inputMsgs <- message - - // Respond - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "text/plain") - - _, err = w.Write([]byte("Rodger that!")) - if err != nil { - log.Error().Msgf("failed to send response: %v", err) - } - - log.Info().Msgf("triggering rule: %s", rule.Name) - } -} - // getRooms - return a map of rooms. func getRooms(api *slack.Client) map[string]string { rooms := make(map[string]string) @@ -603,31 +442,6 @@ func populateMessage(message models.Message, msgType models.MessageType, channel } } -// processInteractiveComponentRule processes a rule that was triggered by an interactive component, e.g. Slack interactive messages. -func processInteractiveComponentRule(rule models.Rule, message *models.Message) { - // Get slack attachments from hit rule and append to outgoing message - config := rule.Remotes.Slack - if config.Attachments != nil { - log.Debug().Msgf("found attachment for rule %#q", rule.Name) - - config.Attachments[0].CallbackID = message.ID - - if len(config.Attachments[0].Actions) > 0 { - for i, action := range config.Attachments[0].Actions { - actionValue, err := utils.Substitute(action.Value, message.Vars) - if err != nil { - log.Warn().Msg(err.Error()) - } - - config.Attachments[0].Actions[i].Value = actionValue - } - } - - message.Remotes.Slack.Attachments = config.Attachments - message.IsEphemeral = true // We default Slack Message attachment's as ephemeral - } -} - // readFromEventsAPI utilizes the Slack API client to read event-based messages. // This method of reading is preferred over the RTM method. func readFromEventsAPI(api *slack.Client, vToken string, inputMsgs chan<- models.Message, bot *models.Bot) { diff --git a/remote/slack/remote.go b/remote/slack/remote.go index ef632b3b..9f1e0b5f 100644 --- a/remote/slack/remote.go +++ b/remote/slack/remote.go @@ -5,9 +5,6 @@ package slack import ( - "net/http" - - "github.com/gorilla/mux" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/slack-go/slack" @@ -155,55 +152,3 @@ func (c *Client) Send(message models.Message, bot *models.Bot) { log.Warn().Msg("received unknown message type - no message to send") } } - -var interactionsRouter *mux.Router - -// InteractiveComponents implementation to satisfy remote interface -// It will serve as a way for your bot to handle advance messaging, such as message attachments. -// When your bot is up and running, it will have an http/https endpoint to handle rules for sending attachments. -func (c *Client) InteractiveComponents(inputMsgs chan<- models.Message, message *models.Message, rule models.Rule, bot *models.Bot) { - if bot.InteractiveComponents && c.SigningSecret != "" { - if bot.SlackInteractionsCallbackPath == "" { - log.Error().Msg("need to specify a callback path for the 'slack_interactions_callback_path' field in the bot.yml (e.g. \"/slack_events/v1/mybot_dev-v1_interactions\")") - log.Warn().Msg("closing interactions reader (will not be able to read interactive components)") - - return - } - - if interactionsRouter == nil { - // create router for the Interactive Components server - interactionsRouter = mux.NewRouter() - - // interaction health check handler - interactionsRouter.HandleFunc("/interaction_health", getInteractiveComponentHealthHandler(bot)).Methods("GET") - - // Rule handler and endpoint - ruleHandle := getInteractiveComponentRuleHandler(inputMsgs, rule, bot) - - // We use regex for interactions routing for any bot using this framework - // e.g. /slack_events/v1/mybot_dev-v1_interactions - if !isValidPath(bot.SlackInteractionsCallbackPath) { - log.Error().Msg(`invalid events path - please double check your path value/syntax (e.g. "/slack_events/v1/mybot_dev-v1_interactions")`) - log.Warn().Msg("closing interaction components reader (will not be able to read interactive components)") - - return - } - - interactionsRouter.HandleFunc(bot.SlackInteractionsCallbackPath, ruleHandle).Methods("POST") - - // start Interactive Components server - go func() { - //nolint:gosec // fix to use server with timeout - err := http.ListenAndServe(":4000", interactionsRouter) - if err != nil { - log.Error().Msgf("unable to start interactions endpoint: %v", err) - } - }() - - log.Info().Msgf("slack interactive components server is listening to %#q", bot.SlackInteractionsCallbackPath) - } - - // Process the hit rule for Interactive Components, e.g. interactive messages - processInteractiveComponentRule(rule, message) - } -} diff --git a/remote/slack/util.go b/remote/slack/util.go index 5196fb60..36142549 100644 --- a/remote/slack/util.go +++ b/remote/slack/util.go @@ -6,7 +6,6 @@ package slack import ( "fmt" - "net/url" "regexp" "strings" @@ -54,20 +53,6 @@ func getMessageType(channel string) (models.MessageType, error) { return models.MsgTypeUnknown, fmt.Errorf("unable to handle channel: UNKNOWN_%s", channel) } -// isValidPath - regex matches a URL's path string to check if it is a correct path. -func isValidPath(path string) bool { - pathPattern := regexp.MustCompile(`^([a-z][a-z0-9+\-.]*:(//[^/?#]+)?)?([a-z0-9\-._~%!$&'()*+,;=:@/]*)`) - matches := pathPattern.FindAllString(path, -1) - - if matches != nil { - if matches[0] == path { - return true - } - } - - return false -} - // removeBotMention - parse out the prepended bot mention in a message. func removeBotMention(contents, botID string) (string, bool) { mention := fmt.Sprintf("<@%s> ", botID) @@ -81,18 +66,3 @@ func removeBotMention(contents, botID string) (string, bool) { return contents, wasMentioned } - -// sanitizeContents - sanitizes a buffer's contents from incoming http payloads. -func sanitizeContents(b []byte) (string, error) { - contents := string(b) - contents = strings.Replace(contents, "payload=", "", 1) - - contents, err := url.QueryUnescape(contents) - if err != nil { - return "", err - } - - contents = strings.ReplaceAll(contents, `\/`, `/`) - - return contents, nil -} diff --git a/remote/telegram/remote.go b/remote/telegram/remote.go index 12a81625..c8752625 100644 --- a/remote/telegram/remote.go +++ b/remote/telegram/remote.go @@ -160,8 +160,3 @@ func (c *Client) Send(message models.Message, bot *models.Bot) { log.Error().Msgf("unable to send message: %v", err) } } - -// InteractiveComponents implementation to satisfy remote interface. -func (c *Client) InteractiveComponents(inputMsgs chan<- models.Message, message *models.Message, rule models.Rule, bot *models.Bot) { - // not implemented for Telegram -} diff --git a/testdata/goodbot.yml b/testdata/goodbot.yml index 0f7e7642..6a53edff 100644 --- a/testdata/goodbot.yml +++ b/testdata/goodbot.yml @@ -24,14 +24,6 @@ debug: true # true: enable logging to console # false: disable logging -log_json: false -# true: logs will be in JOSN format -# false: logs will be in plain text - -interactive_components: false -# true: enables users to create and interact with chat platforms interactive components (e.g. Slack message attachments) -# false (defualt): disables interactive components for all supported chat platform - # for prometheus metrics capabilities metrics: false # true: enables prometheus metrics on localhost port 8080