From 88459577f42e6400631e4c213a32aef16bb235f0 Mon Sep 17 00:00:00 2001 From: Shashank Shekhar Date: Thu, 30 Nov 2023 11:56:32 +0530 Subject: [PATCH] TP-49403 | parameterized slash command (#297) * TP-49403 | parameterized slash command * TP-49403 | handeling resolve and rca params also implemented Help-Commands button * TP-49403 | using command pattern for command resolutiuon and execution * TP-49403 | made find team by name and find severity by name queries case insensitive * TP-49403 | updating help message keys --- .gitignore | 3 +- appcontext/app.go | 130 +++++++++++ cmd/app/handler/slack_handler.go | 35 +-- cmd/main.go | 15 +- common/util/common_util.go | 18 ++ common/util/constant.go | 1 + config/application.properties | 5 +- internal/houston_commands.go | 18 ++ .../action/help_commands_submit_action.go | 59 +++++ ...nd_action.go => houston_command_action.go} | 37 +-- .../incident_update_description_action.go | 2 +- .../incident_update_jira-links_action.go | 2 +- .../incident_update_resolution_text_action.go | 2 +- .../action/incident_update_severity_action.go | 2 +- .../action/incident_update_status_action.go | 2 +- .../action/incident_update_type_action.go | 2 +- ...t_description_view_modal_command_action.go | 65 ++++++ .../open_set_rca_view_modal_command_action.go | 65 ++++++ ..._set_severity_view_modal_command_action.go | 66 ++++++ ...en_set_status_view_modal_command_action.go | 65 ++++++ ...open_set_team_view_modal_command_action.go | 65 ++++++ .../action/resolve_incident_command_action.go | 176 ++++++++++++++ .../action/set_description_command_action.go | 85 +++++++ .../action/set_severity_command_action.go | 146 ++++++++++++ .../action/set_status_command_action.go | 162 +++++++++++++ .../action/set_team_command_action.go | 146 ++++++++++++ .../action/start_incident_command_action.go | 220 ++++++++++++++++++ .../action/view/incident_description.go | 4 +- .../action/view/incident_jira_links.go | 4 +- .../action/view/incident_resolution_text.go | 4 +- .../processor/action/view/incident_section.go | 2 +- .../action/view/incident_severity.go | 4 +- .../view/incident_status_update_modal.go | 4 +- .../action/view/incident_update_type.go | 4 +- .../event_type_interactive_processor.go | 9 +- internal/processor/help_command_processor.go | 39 ++++ .../processor/houston_command_processor.go | 38 +++ ...escription_view_modal_command_processor.go | 36 +++ ...en_set_rca_view_modal_command_processor.go | 36 +++ ...t_severity_view_modal_command_processor.go | 36 +++ ...set_status_view_modal_command_processor.go | 36 +++ ...n_set_team_view_modal_command_processor.go | 36 +++ .../resolve_incident_command_processor.go | 39 ++++ .../set_description_command_processor.go | 36 +++ .../set_severity_command_processor.go | 36 +++ .../processor/set_status_command_processor.go | 36 +++ .../processor/set_team_command_processor.go | 36 +++ internal/processor/slash_command_processor.go | 11 +- .../start_incident_command_processor.go | 36 +++ internal/resolver/houston_command_resolver.go | 138 +++++++++++ internal/resolver/slash_command_resolver.go | 38 --- model/severity/severity.go | 2 +- model/team/team.go | 3 +- service/incident/incident_service_v2.go | 26 ++- .../incident/incident_service_v2_interface.go | 1 + 55 files changed, 2212 insertions(+), 112 deletions(-) create mode 100644 appcontext/app.go create mode 100644 internal/houston_commands.go create mode 100644 internal/processor/action/help_commands_submit_action.go rename internal/processor/action/{slash_command_action.go => houston_command_action.go} (59%) create mode 100644 internal/processor/action/open_set_description_view_modal_command_action.go create mode 100644 internal/processor/action/open_set_rca_view_modal_command_action.go create mode 100644 internal/processor/action/open_set_severity_view_modal_command_action.go create mode 100644 internal/processor/action/open_set_status_view_modal_command_action.go create mode 100644 internal/processor/action/open_set_team_view_modal_command_action.go create mode 100644 internal/processor/action/resolve_incident_command_action.go create mode 100644 internal/processor/action/set_description_command_action.go create mode 100644 internal/processor/action/set_severity_command_action.go create mode 100644 internal/processor/action/set_status_command_action.go create mode 100644 internal/processor/action/set_team_command_action.go create mode 100644 internal/processor/action/start_incident_command_action.go create mode 100644 internal/processor/help_command_processor.go create mode 100644 internal/processor/houston_command_processor.go create mode 100644 internal/processor/open_set_description_view_modal_command_processor.go create mode 100644 internal/processor/open_set_rca_view_modal_command_processor.go create mode 100644 internal/processor/open_set_severity_view_modal_command_processor.go create mode 100644 internal/processor/open_set_status_view_modal_command_processor.go create mode 100644 internal/processor/open_set_team_view_modal_command_processor.go create mode 100644 internal/processor/resolve_incident_command_processor.go create mode 100644 internal/processor/set_description_command_processor.go create mode 100644 internal/processor/set_severity_command_processor.go create mode 100644 internal/processor/set_status_command_processor.go create mode 100644 internal/processor/set_team_command_processor.go create mode 100644 internal/processor/start_incident_command_processor.go create mode 100644 internal/resolver/houston_command_resolver.go delete mode 100644 internal/resolver/slash_command_resolver.go diff --git a/.gitignore b/.gitignore index 488e807..7b9e112 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ go.sum -.DS_STORE \ No newline at end of file +.DS_STORE +*.env \ No newline at end of file diff --git a/appcontext/app.go b/appcontext/app.go new file mode 100644 index 0000000..b09ebbd --- /dev/null +++ b/appcontext/app.go @@ -0,0 +1,130 @@ +package appcontext + +import ( + "github.com/slack-go/slack/socketmode" + "github.com/spf13/viper" + "gorm.io/gorm" + "houston/model/incident" + "houston/model/log" + "houston/model/severity" + "houston/model/tag" + "houston/model/team" + "houston/pkg/postgres" + incidentService "houston/service/incident" + "houston/service/slack" +) + +type applicationContext struct { + db *gorm.DB +} + +// todo: all the repo objects to be cleaned uo from here. appContext will only have services +type houstonServices struct { + logRepo *log.Repository + teamRepo *team.Repository + severityRepo *severity.Repository + incidentRepo *incident.Repository + slackService *slack.SlackService + incidentService *incidentService.IncidentServiceV2 + tagRepo *tag.Repository +} + +var appContext *applicationContext +var services *houstonServices + +func InitiateContext() { + appContext = &applicationContext{ + db: initDB(), + } +} + +func InitializeServices() { + logRepo := initLogRepo() + severityRepo := initSeverityRepo() + teamRepo := initTeamRepo(logRepo) + slaService := initSlackService() + services = &houstonServices{ + logRepo: logRepo, + teamRepo: teamRepo, + severityRepo: severityRepo, + incidentRepo: initIncidentRepo(severityRepo, logRepo, teamRepo, slaService.SocketModeClient), + slackService: slaService, + incidentService: initIncidentService(), + tagRepo: initTagRepo(), + } +} + +func initDB() *gorm.DB { + return postgres.NewGormClient( + viper.GetString("postgres.dsn"), + viper.GetString("postgres.connection.max.idle.time"), + viper.GetString("postgres.connection.max.lifetime"), + viper.GetInt("postgres.connections.max.idle"), + viper.GetInt("postgres.connections.max.open"), + ) +} + +func GetDB() *gorm.DB { + return appContext.db +} + +func initLogRepo() *log.Repository { + return log.NewLogRepository(GetDB()) +} + +func GetLogRepo() *log.Repository { + return services.logRepo +} + +func initTeamRepo(logRepo *log.Repository) *team.Repository { + return team.NewTeamRepository(GetDB(), logRepo) +} + +func GetTeamRepo() *team.Repository { + return services.teamRepo +} + +func initSeverityRepo() *severity.Repository { + return severity.NewSeverityRepository(GetDB()) +} + +func GetSeverityRepo() *severity.Repository { + return services.severityRepo +} + +func initIncidentRepo( + severityRepo *severity.Repository, + logRepo *log.Repository, + teamRepo *team.Repository, + socketModeClient *socketmode.Client, +) *incident.Repository { + return incident.NewIncidentRepository(GetDB(), severityRepo, logRepo, teamRepo, socketModeClient) +} + +func GetIncidentRepo() *incident.Repository { + return services.incidentRepo +} + +func initSlackService() *slack.SlackService { + return slack.NewSlackService() +} + +func GetSlackService() *slack.SlackService { + return services.slackService +} + +func initIncidentService() *incidentService.IncidentServiceV2 { + return incidentService.NewIncidentServiceV2(GetDB()) +} + +func GetIncidentService() *incidentService.IncidentServiceV2 { + return services.incidentService +} + +func initTagRepo() *tag.Repository { + return tag.NewTagRepository(GetDB()) +} + +func GetTagRepo() *tag.Repository { + return services.tagRepo +} diff --git a/cmd/app/handler/slack_handler.go b/cmd/app/handler/slack_handler.go index c36d865..e7fa997 100644 --- a/cmd/app/handler/slack_handler.go +++ b/cmd/app/handler/slack_handler.go @@ -35,12 +35,13 @@ import ( type slackHandler struct { socketModeClient *socketmode.Client slashCommandProcessor *processor.SlashCommandProcessor + commandProcessor processor.CommandProcessor memberJoinCallbackProcessor *processor.MemberJoinedCallbackEventProcessor blockActionProcessor *processor.BlockActionProcessor viewSubmissionProcessor *processor.ViewSubmissionProcessor - slashCommandResolver *resolver.SlashCommandResolver diagnosticCommandProcessor *processor.DiagnosticCommandProcessor userChangeEventProcessor *processor.UserChangeEventProcessor + houstonCommandResolver *resolver.HoustonCommandResolver } func NewSlackHandler(gormClient *gorm.DB, socketModeClient *socketmode.Client) *slackHandler { @@ -52,7 +53,6 @@ func NewSlackHandler(gormClient *gorm.DB, socketModeClient *socketmode.Client) * userService := user.NewUserRepository(gormClient) shedlockService := shedlock.NewShedlockRepository(gormClient) slackbotClient := slackbot.NewSlackClient(socketModeClient) - slashCommandProcessor := processor.NewSlashCommandProcessor(socketModeClient, incidentService, slackbotClient) grafanaRepository := diagnostic.NewDiagnoseRepository(gormClient) diagnosticCommandProcessor := processor.NewDiagnosticCommandProcessor(socketModeClient, grafanaRepository) incidentServiceV2 := incidentServiceV2.NewIncidentServiceV2(gormClient) @@ -63,18 +63,21 @@ func NewSlackHandler(gormClient *gorm.DB, socketModeClient *socketmode.Client) * restClient := rest.NewClientActionsImpl() documentService := documentService.NewActionsImpl(restClient) rcaService := rca.NewRcaService(incidentServiceV2, slackService, restClient, documentService, rcaRepository, rcaInputRepository, userService) + slashCommandProcessor := processor.NewSlashCommandProcessor(socketModeClient, slackbotClient, rcaService) - cron.RunJob( - socketModeClient, - gormClient, - incidentService, - severityService, - teamService, - shedlockService, - userService, - incidentServiceV2, - slackService, - ) + if viper.GetString("env") != "local" { + cron.RunJob( + socketModeClient, + gormClient, + incidentService, + severityService, + teamService, + shedlockService, + userService, + incidentServiceV2, + slackService, + ) + } return &slackHandler{ socketModeClient: socketModeClient, @@ -89,12 +92,10 @@ func NewSlackHandler(gormClient *gorm.DB, socketModeClient *socketmode.Client) * viewSubmissionProcessor: processor.NewViewSubmissionProcessor( socketModeClient, incidentService, teamService, severityService, tagService, teamService, slackbotClient, gormClient, ), - slashCommandResolver: resolver.NewSlashCommandResolver( - diagnosticCommandProcessor, slashCommandProcessor, - ), userChangeEventProcessor: processor.NewUserChangeEventProcessor( socketModeClient, userService, ), + houstonCommandResolver: resolver.NewHoustonCommandResolver(diagnosticCommandProcessor, socketModeClient, slackbotClient, rcaService), } } @@ -182,7 +183,7 @@ func (sh *slackHandler) HoustonConnect() { } case socketmode.EventTypeSlashCommand: { - sh.slashCommandResolver.Resolve(&evt) + sh.houstonCommandResolver.Resolve(&evt).ProcessSlashCommand(&evt) } default: { diff --git a/cmd/main.go b/cmd/main.go index 6cbab43..2ae8954 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,11 +1,11 @@ package main import ( + "houston/appcontext" "houston/cmd/app" "houston/config" "houston/internal/clients" "houston/logger" - "houston/pkg/postgres" "os" "time" @@ -19,9 +19,11 @@ import ( ) func main() { - logger.InitLogger() config.LoadHoustonConfig() godotenv.Load() + logger.InitLogger() + appcontext.InitiateContext() + appcontext.InitializeServices() command := &cobra.Command{ Use: "houston", @@ -35,20 +37,13 @@ func main() { r.Use(ginzap.RecoveryWithZap(logger.GetLogger(), true)) houston := r.Group("/houston") - db := postgres.NewGormClient( - viper.GetString("postgres.dsn"), - viper.GetString("postgres.connection.max.idle.time"), - viper.GetString("postgres.connection.max.lifetime"), - viper.GetInt("postgres.connections.max.idle"), - viper.GetInt("postgres.connections.max.open"), - ) httpClient := clients.NewHttpClient() mjolnirClient := clients.NewMjolnirClient( httpClient.HttpClient, viper.GetString("mjolnir.service.url"), viper.GetString("mjolnir.realm.id"), ) - sv := app.NewServer(r, db, mjolnirClient) + sv := app.NewServer(r, appcontext.GetDB(), mjolnirClient) sv.Handler(houston) sv.Start() diff --git a/common/util/common_util.go b/common/util/common_util.go index be2147b..4edf34c 100644 --- a/common/util/common_util.go +++ b/common/util/common_util.go @@ -13,9 +13,27 @@ import ( "houston/model/team" request "houston/service/request" "math" + "strings" "time" ) +func SplitUntilWord(input, stopWord string) (string, string) { + // Convert both input and stopWord to lowercase for case-insensitive matching + lowercaseInput := strings.ToLower(input) + lowercaseStopWord := strings.ToLower(stopWord) + + // Find the index of the stopWord in the lowercase input + stopIndex := strings.Index(lowercaseInput, lowercaseStopWord) + + if stopIndex == -1 { + // If stopWord is not found, return the entire input as the first part + return input, "" + } + + // Return the part of the input before the stopWord and the part after the stopWord + return strings.TrimSpace(input[:stopIndex]), strings.TrimSpace(input[stopIndex+len(stopWord):]) +} + func RemoveDuplicate[T string | int](sliceList []T) []T { allKeys := make(map[T]bool) list := []T{} diff --git a/common/util/constant.go b/common/util/constant.go index 58b9b12..b8dc062 100644 --- a/common/util/constant.go +++ b/common/util/constant.go @@ -5,6 +5,7 @@ type BlockActionType string const ( StartIncident BlockActionType = "start_incident" ShowIncidents = "show_incidents" + HelpCommand = "help_button" Incident = "incident" Tags = "tags" AssignIncidentRole = "assign_incident_role" diff --git a/config/application.properties b/config/application.properties index 9d66819..9ddd6d8 100644 --- a/config/application.properties +++ b/config/application.properties @@ -85,4 +85,7 @@ create-incident.title.max-length=100 create-incident-v2-enabled=CREATE_INCIDENT_V2_ENABLED #slack details slack.workspace.id=SLACK_WORKSPACE_ID -navi.jira.base.url=https://navihq.atlassian.net/ \ No newline at end of file +navi.jira.base.url=https://navihq.atlassian.net/ + +houston.channel.help.message=```/houston: General command to open the UI shell which contains other categorized commands| /houston severity: Opens the view to update severity of the incident| /houston set severity to : Sets the incident severity| /houston team: Opens the view to update team| /houston set team to : Sets the incident team| /houston status: Opens the view to set status| /houston set status to : Sets the incident status| /houston description: Opens the view to set incident description| /houston set description to : Sets the incident description| /houston resolve: Opens the view to fill RCA and resolve| /houston rca: Opens the view to fill RCA``` +non.houston.channel.help.message=```/houston: General command to open the UI shell which contains other categorized commands| /houston start: Opens the view to start a new incident| /houston start title description : Starts an incident of a specific severity and team``` \ No newline at end of file diff --git a/internal/houston_commands.go b/internal/houston_commands.go new file mode 100644 index 0000000..9fbe62d --- /dev/null +++ b/internal/houston_commands.go @@ -0,0 +1,18 @@ +package internal + +type houstonCommandParameter string + +const ( + StartIncidentParam houstonCommandParameter = "start" + OpenSeverityModalParam = "severity" + SetSeverityParam = "set severity to" + OpenTeamModalParam = "team" + SetTeamParam = "set team to" + OpenStatusModalParam = "status" + SetStatusParam = "set status to" + OpenDescriptionModalParam = "description" + SetDescriptionParam = "set description to" + ResolveIncidentParam = "resolve" + OpenRCAModalParam = "rca" + HelpParam = "help" +) diff --git a/internal/processor/action/help_commands_submit_action.go b/internal/processor/action/help_commands_submit_action.go new file mode 100644 index 0000000..5234ba7 --- /dev/null +++ b/internal/processor/action/help_commands_submit_action.go @@ -0,0 +1,59 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "github.com/spf13/viper" + "houston/appcontext" + "houston/common/util" + "houston/logger" + "strings" +) + +type HelpCommandsAction struct { + socketModeClient *socketmode.Client +} + +func NewHelpCommandsAction(client *socketmode.Client) *HelpCommandsAction { + return &HelpCommandsAction{ + socketModeClient: client, + } +} + +const helpCommandsLogTag = "[help_commands_action]" + +func (action *HelpCommandsAction) ProcessAction(userID, channelID string, request *socketmode.Request) { + + isHoustonChannel, err := appcontext.GetIncidentService().IsHoustonChannel(channelID) + if err != nil { + appcontext.GetSlackService().PostEphemeralByChannelID( + "Something went wrong, please retry in sometime.", + userID, + false, + channelID, + ) + return + } + + var helpMessageKey string + + if isHoustonChannel { + helpMessageKey = "houston.channel.help.message" + } else { + helpMessageKey = "non.houston.channel.help.message" + } + var helpMessage []string + helpMessage = strings.Split(viper.GetString(helpMessageKey), "|") + + var formattedHelpMessage string + for _, h := range helpMessage { + command, helpText := util.SplitUntilWord(h, ":") + formattedHelpMessage += fmt.Sprintf("%-91s: %s\n", strings.TrimSpace(command), strings.TrimSpace(helpText)) + } + err = appcontext.GetSlackService().PostEphemeralByChannelID(formattedHelpMessage, userID, false, channelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post help command response. %+v", helpCommandsLogTag, err)) + } + var payload interface{} + action.socketModeClient.Ack(*request, payload) +} diff --git a/internal/processor/action/slash_command_action.go b/internal/processor/action/houston_command_action.go similarity index 59% rename from internal/processor/action/slash_command_action.go rename to internal/processor/action/houston_command_action.go index 94c49df..2ba4de0 100644 --- a/internal/processor/action/slash_command_action.go +++ b/internal/processor/action/houston_command_action.go @@ -1,31 +1,36 @@ package action import ( - "houston/internal/processor/action/view" - "houston/logger" - "houston/model/incident" - "houston/pkg/slackbot" - + "fmt" "github.com/slack-go/slack" "github.com/slack-go/slack/socketmode" "go.uber.org/zap" + "houston/appcontext" + "houston/internal/processor/action/view" + "houston/logger" + "houston/pkg/slackbot" + "houston/service/rca" ) -type SlashCommandAction struct { - incidentService *incident.Repository +type HoustonCommandAction struct { socketModeClient *socketmode.Client slackBot *slackbot.Client + rcaService *rca.RcaService } -func NewSlashCommandAction(service *incident.Repository, socketModeClient *socketmode.Client, slackBot *slackbot.Client) *SlashCommandAction { - return &SlashCommandAction{ - incidentService: service, +func NewHoustonCommandAction( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, + rcaService *rca.RcaService, +) *HoustonCommandAction { + return &HoustonCommandAction{ socketModeClient: socketModeClient, slackBot: slackBot, + rcaService: rcaService, } } -func (sca *SlashCommandAction) PerformAction(evt *socketmode.Event) { +func (action *HoustonCommandAction) PerformAction(evt *socketmode.Event) { cmd, ok := evt.Data.(slack.SlashCommand) logger.Info("processing houston command", zap.Any("payload", cmd)) if !ok { @@ -33,7 +38,7 @@ func (sca *SlashCommandAction) PerformAction(evt *socketmode.Event) { return } - result, err := sca.incidentService.FindIncidentByChannelId(cmd.ChannelID) + result, err := appcontext.GetIncidentRepo().FindIncidentByChannelId(cmd.ChannelID) if err != nil { logger.Error("FindIncidentBySlackChannelId errors", zap.String("channel_id", cmd.ChannelID), zap.String("channel", cmd.ChannelName), @@ -44,14 +49,16 @@ func (sca *SlashCommandAction) PerformAction(evt *socketmode.Event) { if result != nil { logger.Info("Result", zap.String("result", result.IncidentName)) payload := view.ExistingIncidentOptionsBlock() - sca.socketModeClient.Ack(*evt.Request, payload) + action.socketModeClient.Ack(*evt.Request, payload) } - _, err = sca.slackBot.GetConversationInfo(cmd.ChannelID) + _, err = action.slackBot.GetConversationInfo(cmd.ChannelID) var payload map[string]interface{} if err != nil && err.Error() == "channel_not_found" { payload = view.IntegrateHoustonInChannelBlock() } else { payload = view.NewIncidentBlock() } - sca.socketModeClient.Ack(*evt.Request, payload) + action.socketModeClient.Ack(*evt.Request, payload) } + +var genericBackendError = fmt.Errorf("something went wrong. Please retry after sometime or run `/houston` command") diff --git a/internal/processor/action/incident_update_description_action.go b/internal/processor/action/incident_update_description_action.go index 3f0c5f5..a621539 100644 --- a/internal/processor/action/incident_update_description_action.go +++ b/internal/processor/action/incident_update_description_action.go @@ -36,7 +36,7 @@ func (idp *IncidentUpdateDescriptionAction) IncidentUpdateDescriptionRequestProc zap.String("user_id", callback.User.ID), zap.Error(err)) return } - modalRequest := view.BuildIncidentUpdateDescriptionModal(callback.Channel, result.Description) + modalRequest := view.BuildIncidentUpdateDescriptionModal(callback.Channel.ID, result.Description) _, err = idp.client.OpenView(callback.TriggerID, modalRequest) if err != nil { diff --git a/internal/processor/action/incident_update_jira-links_action.go b/internal/processor/action/incident_update_jira-links_action.go index 40e3cd0..62e2de1 100644 --- a/internal/processor/action/incident_update_jira-links_action.go +++ b/internal/processor/action/incident_update_jira-links_action.go @@ -45,7 +45,7 @@ func (action *IncidentUpdateJiraLinksAction) IncidentUpdateJiraLinksRequestProce return } - modalRequest := view.BuildJiraLinksModal(callback.Channel, result.JiraLinks...) + modalRequest := view.BuildJiraLinksModal(callback.Channel.ID, result.JiraLinks...) _, err = action.client.OpenView(callback.TriggerID, modalRequest) if err != nil { diff --git a/internal/processor/action/incident_update_resolution_text_action.go b/internal/processor/action/incident_update_resolution_text_action.go index 782ef6f..20a4cab 100644 --- a/internal/processor/action/incident_update_resolution_text_action.go +++ b/internal/processor/action/incident_update_resolution_text_action.go @@ -37,7 +37,7 @@ func (idp *IncidentUpdateRcaAction) IncidentUpdateRcaRequestProcess(callback sla zap.String("user_id", callback.User.ID), zap.Error(err)) return } - modalRequest := view.BuildRcaModal(callback.Channel, result.RCA) + modalRequest := view.BuildRcaModal(callback.Channel.ID, result.RCA) _, err = idp.client.OpenView(callback.TriggerID, modalRequest) if err != nil { diff --git a/internal/processor/action/incident_update_severity_action.go b/internal/processor/action/incident_update_severity_action.go index f84c8e3..b90b22f 100644 --- a/internal/processor/action/incident_update_severity_action.go +++ b/internal/processor/action/incident_update_severity_action.go @@ -43,7 +43,7 @@ func (isp *IncidentUpdateSevertityAction) IncidentUpdateSeverityRequestProcess(c zap.String("user_id", callback.User.ID), zap.Error(err)) return } - modalRequest := view.BuildIncidentUpdateSeverityModal(callback.Channel, *incidentSeverity) + modalRequest := view.BuildIncidentUpdateSeverityModal(callback.Channel.ID, *incidentSeverity) _, err = isp.client.OpenView(callback.TriggerID, modalRequest) if err != nil { diff --git a/internal/processor/action/incident_update_status_action.go b/internal/processor/action/incident_update_status_action.go index ac9f505..75affd0 100644 --- a/internal/processor/action/incident_update_status_action.go +++ b/internal/processor/action/incident_update_status_action.go @@ -42,7 +42,7 @@ func (isp *UpdateIncidentAction) IncidentUpdateStatusRequestProcess(callback sla return } - modalRequest := view.BuildIncidentUpdateStatusModal(*incidentStatuses, callback.Channel) + modalRequest := view.BuildIncidentUpdateStatusModal(*incidentStatuses, callback.Channel.ID) _, err = isp.client.OpenView(callback.TriggerID, modalRequest) if err != nil { diff --git a/internal/processor/action/incident_update_type_action.go b/internal/processor/action/incident_update_type_action.go index 873cd6d..7426865 100644 --- a/internal/processor/action/incident_update_type_action.go +++ b/internal/processor/action/incident_update_type_action.go @@ -43,7 +43,7 @@ func (incidentUpdateTypeAction *IncidentUpdateTypeAction) IncidentUpdateTypeRequ return } - modalRequest := view.BuildIncidentUpdateTypeModal(callback.Channel, *teams) + modalRequest := view.BuildIncidentUpdateTypeModal(callback.Channel.ID, *teams) _, err = incidentUpdateTypeAction.socketModeClient.OpenView(callback.TriggerID, modalRequest) if err != nil { diff --git a/internal/processor/action/open_set_description_view_modal_command_action.go b/internal/processor/action/open_set_description_view_modal_command_action.go new file mode 100644 index 0000000..6f44a91 --- /dev/null +++ b/internal/processor/action/open_set_description_view_modal_command_action.go @@ -0,0 +1,65 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/appcontext" + "houston/internal/processor/action/view" + "houston/logger" + "houston/pkg/slackbot" +) + +const openSetDescriptionViewModalActionLogTag = "[open_set_description_view_modal_command_action]" + +type OpenSetDescriptionViewModalCommandAction struct { + socketModeClient *socketmode.Client + slackBot *slackbot.Client +} + +func NewOpenSetDescriptionViewModalCommandAction( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *OpenSetDescriptionViewModalCommandAction { + return &OpenSetDescriptionViewModalCommandAction{ + socketModeClient: socketModeClient, + slackBot: slackBot, + } +} + +func (action *OpenSetDescriptionViewModalCommandAction) PerformAction(evt *socketmode.Event) { + cmd, ok := evt.Data.(slack.SlashCommand) + logger.Info("processing houston command", zap.Any("payload", cmd)) + if !ok { + logger.Error("event data to slash command conversion failed", zap.Any("data", evt)) + return + } + + err := action.openSetDescriptionViewModal(cmd) + if err != nil { + err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", openSetDescriptionViewModalActionLogTag, err)) + } + } + + action.socketModeClient.Ack(*evt.Request) +} + +// todo: this method has to be removed and usage has to be replaced with update incident V2 once update incident refactor goes live. +func (action *OpenSetDescriptionViewModalCommandAction) openSetDescriptionViewModal(cmd slack.SlashCommand) error { + logger.Info("opening set description view modal") + return executeForHoustonChannel(cmd, func() error { + incidentEntity, err := appcontext.GetIncidentService().GetIncidentByChannelID(cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch incident by channel ID: %s. %+v", openSetDescriptionViewModalActionLogTag, cmd.ChannelID, err)) + return fmt.Errorf("failed to fetch incident by channel ID: %s", cmd.ChannelID) + } + _, err = action.socketModeClient.OpenView(cmd.TriggerID, view.BuildIncidentUpdateDescriptionModal(cmd.ChannelID, incidentEntity.Description)) + if err != nil { + return fmt.Errorf("failed to open set description view modal: %s", err.Error()) + } + return nil + }) +} diff --git a/internal/processor/action/open_set_rca_view_modal_command_action.go b/internal/processor/action/open_set_rca_view_modal_command_action.go new file mode 100644 index 0000000..6a1d10c --- /dev/null +++ b/internal/processor/action/open_set_rca_view_modal_command_action.go @@ -0,0 +1,65 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/appcontext" + "houston/internal/processor/action/view" + "houston/logger" + "houston/pkg/slackbot" +) + +const openSetRCAViewModalActionLogTag = "[open_set_rca_view_modal_command_action]" + +type OpenFillRCAViewModalCommandAction struct { + socketModeClient *socketmode.Client + slackBot *slackbot.Client +} + +func NewOpenFillRCAViewModalCommandAction( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *OpenFillRCAViewModalCommandAction { + return &OpenFillRCAViewModalCommandAction{ + socketModeClient: socketModeClient, + slackBot: slackBot, + } +} + +func (action *OpenFillRCAViewModalCommandAction) PerformAction(evt *socketmode.Event) { + cmd, ok := evt.Data.(slack.SlashCommand) + logger.Info("processing houston command", zap.Any("payload", cmd)) + if !ok { + logger.Error("event data to slash command conversion failed", zap.Any("data", evt)) + return + } + + err := action.openFillRCAViewModal(cmd) + if err != nil { + err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", openSetRCAViewModalActionLogTag, err)) + } + } + + action.socketModeClient.Ack(*evt.Request) +} + +func (action *OpenFillRCAViewModalCommandAction) openFillRCAViewModal(cmd slack.SlashCommand) error { + logger.Info(fmt.Sprintf("%s received request to resolve the incident", openSetRCAViewModalActionLogTag)) + + return executeForHoustonChannel(cmd, func() error { + incidentEntity, err := appcontext.GetIncidentService().GetIncidentByChannelID(cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch incident by channel ID: %s. %+v", openSetRCAViewModalActionLogTag, cmd.ChannelID, err)) + return genericBackendError + } + _, err = action.socketModeClient.OpenView(cmd.TriggerID, view.BuildRcaModal(cmd.ChannelID, incidentEntity.RCA)) + if err != nil { + return fmt.Errorf("failed to open set RCA view modal") + } + return nil + }) +} diff --git a/internal/processor/action/open_set_severity_view_modal_command_action.go b/internal/processor/action/open_set_severity_view_modal_command_action.go new file mode 100644 index 0000000..89acf51 --- /dev/null +++ b/internal/processor/action/open_set_severity_view_modal_command_action.go @@ -0,0 +1,66 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/appcontext" + "houston/internal/processor/action/view" + "houston/logger" + "houston/pkg/slackbot" +) + +const openSetSeverityViewModalActionLogTag = "[open_set_severity_view_modal_command_action]" + +type OpenSetSeverityViewModalCommandAction struct { + socketModeClient *socketmode.Client + slackBot *slackbot.Client +} + +func NewOpenSetSeverityViewModalCommandAction( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *OpenSetSeverityViewModalCommandAction { + return &OpenSetSeverityViewModalCommandAction{ + socketModeClient: socketModeClient, + slackBot: slackBot, + } +} + +func (action *OpenSetSeverityViewModalCommandAction) PerformAction(evt *socketmode.Event) { + cmd, ok := evt.Data.(slack.SlashCommand) + logger.Info("processing houston command", zap.Any("payload", cmd)) + if !ok { + logger.Error("event data to slash command conversion failed", zap.Any("data", evt)) + return + } + + err := action.openSetSeverityViewModal(cmd) + if err != nil { + err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", setTeamActionLogTag, err)) + } + } + + action.socketModeClient.Ack(*evt.Request) +} + +// todo: this method has to be removed and usage has to be replaced with update incident V2 once update incident refactor goes live. +func (action *OpenSetSeverityViewModalCommandAction) openSetSeverityViewModal(cmd slack.SlashCommand) error { + logger.Info(fmt.Sprintf("%s opening severity view modal", openSetSeverityViewModalActionLogTag)) + return executeForHoustonChannel(cmd, func() error { + severityEntities, err := appcontext.GetSeverityRepo().GetAllActiveSeverity() + if err != nil { + logger.Error(fmt.Sprintf("%s error in fetching all severity entities from DB. %+v", openSetSeverityViewModalActionLogTag, err)) + return genericBackendError + } + _, err = action.socketModeClient.OpenView(cmd.TriggerID, view.BuildIncidentUpdateSeverityModal(cmd.ChannelID, *severityEntities)) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to open set severity view modal: %+v", openSetSeverityViewModalActionLogTag, err)) + return fmt.Errorf("failed to open set severity view modal") + } + return nil + }) +} diff --git a/internal/processor/action/open_set_status_view_modal_command_action.go b/internal/processor/action/open_set_status_view_modal_command_action.go new file mode 100644 index 0000000..f593a37 --- /dev/null +++ b/internal/processor/action/open_set_status_view_modal_command_action.go @@ -0,0 +1,65 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/appcontext" + "houston/internal/processor/action/view" + "houston/logger" + "houston/pkg/slackbot" +) + +const openSetStatusViewModalActionLogTag = "[open_set_status_view_modal_command_action]" + +type OpenSetStatusViewModalCommandAction struct { + socketModeClient *socketmode.Client + slackBot *slackbot.Client +} + +func NewOpenSetStatusViewModalCommandAction( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *OpenSetStatusViewModalCommandAction { + return &OpenSetStatusViewModalCommandAction{ + socketModeClient: socketModeClient, + slackBot: slackBot, + } +} + +func (action *OpenSetStatusViewModalCommandAction) PerformAction(evt *socketmode.Event) { + cmd, ok := evt.Data.(slack.SlashCommand) + logger.Info("processing houston command", zap.Any("payload", cmd)) + if !ok { + logger.Error("event data to slash command conversion failed", zap.Any("data", evt)) + return + } + + err := action.openSetStatusViewModal(cmd) + if err != nil { + err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", openSetStatusViewModalActionLogTag, err)) + } + } + + action.socketModeClient.Ack(*evt.Request) +} + +// todo: this method has to be removed and usage has to be replaced with update incident V2 once update incident refactor goes live. +func (action *OpenSetStatusViewModalCommandAction) openSetStatusViewModal(cmd slack.SlashCommand) error { + logger.Info("opening set status view modal") + return executeForHoustonChannel(cmd, func() error { + statusEntities, err := appcontext.GetIncidentRepo().FetchAllIncidentStatuses() + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch all active teams from DB. %+v", openSetStatusViewModalActionLogTag, err)) + return genericBackendError + } + _, err = action.socketModeClient.OpenView(cmd.TriggerID, view.BuildIncidentUpdateStatusModal(*statusEntities, cmd.ChannelID)) + if err != nil { + return fmt.Errorf("failed to open set status view modal: %s", err.Error()) + } + return nil + }) +} diff --git a/internal/processor/action/open_set_team_view_modal_command_action.go b/internal/processor/action/open_set_team_view_modal_command_action.go new file mode 100644 index 0000000..92722f1 --- /dev/null +++ b/internal/processor/action/open_set_team_view_modal_command_action.go @@ -0,0 +1,65 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/appcontext" + "houston/internal/processor/action/view" + "houston/logger" + "houston/pkg/slackbot" +) + +const openSetTeamViewModalActionLogTag = "[open_set_team_view_modal_command_action]" + +type OpenSetTeamViewModalCommandAction struct { + socketModeClient *socketmode.Client + slackBot *slackbot.Client +} + +func NewOpenSetTeamViewModalCommandAction( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *OpenSetTeamViewModalCommandAction { + return &OpenSetTeamViewModalCommandAction{ + socketModeClient: socketModeClient, + slackBot: slackBot, + } +} + +func (action *OpenSetTeamViewModalCommandAction) PerformAction(evt *socketmode.Event) { + cmd, ok := evt.Data.(slack.SlashCommand) + logger.Info("processing houston command", zap.Any("payload", cmd)) + if !ok { + logger.Error("event data to slash command conversion failed", zap.Any("data", evt)) + return + } + + err := action.openSetTeamViewModal(cmd) + if err != nil { + err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", openSetTeamViewModalActionLogTag, err)) + } + } + + action.socketModeClient.Ack(*evt.Request) +} + +// todo: this method has to be removed and usage has to be replaced with update incident V2 once update incident refactor goes live. +func (action *OpenSetTeamViewModalCommandAction) openSetTeamViewModal(cmd slack.SlashCommand) error { + logger.Info("opening set team view modal") + return executeForHoustonChannel(cmd, func() error { + teamEntities, err := appcontext.GetTeamRepo().GetAllActiveTeams() + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch all active teams from DB. %+v", openSetTeamViewModalActionLogTag, err)) + return fmt.Errorf("failed to fetch all active teams from DB") + } + _, err = action.socketModeClient.OpenView(cmd.TriggerID, view.BuildIncidentUpdateTypeModal(cmd.ChannelID, *teamEntities)) + if err != nil { + return fmt.Errorf("failed to open set team view modal") + } + return nil + }) +} diff --git a/internal/processor/action/resolve_incident_command_action.go b/internal/processor/action/resolve_incident_command_action.go new file mode 100644 index 0000000..a1718f2 --- /dev/null +++ b/internal/processor/action/resolve_incident_command_action.go @@ -0,0 +1,176 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "github.com/spf13/viper" + "go.uber.org/zap" + "houston/appcontext" + "houston/common/util" + "houston/logger" + "houston/model/incident" + "houston/pkg/slackbot" + "houston/service/rca" + "time" +) + +const resolveIncidentActionLogTag = "[slash_command_action]" + +type ResolveIncidentCommandAction struct { + socketModeClient *socketmode.Client + slackBot *slackbot.Client + rcaService *rca.RcaService +} + +func NewResolveIncidentCommandAction( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, + rcaService *rca.RcaService, +) *ResolveIncidentCommandAction { + return &ResolveIncidentCommandAction{ + socketModeClient: socketModeClient, + slackBot: slackBot, + rcaService: rcaService, + } +} + +func (action *ResolveIncidentCommandAction) PerformAction(evt *socketmode.Event) { + cmd, ok := evt.Data.(slack.SlashCommand) + logger.Info("processing houston command", zap.Any("payload", cmd)) + if !ok { + logger.Error("event data to slash command conversion failed", zap.Any("data", evt)) + return + } + + err := action.resolveIncident(cmd) + if err != nil { + err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", resolveIncidentActionLogTag, err)) + } + } + + action.socketModeClient.Ack(*evt.Request) +} + +func (action *ResolveIncidentCommandAction) resolveIncident(cmd slack.SlashCommand) error { + logger.Info(fmt.Sprintf("%s received request to resolve the incident", resolveIncidentActionLogTag)) + + return executeForHoustonChannel(cmd, func() error { + incidentEntity, err := appcontext.GetIncidentService().GetIncidentByChannelID(cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch incident entity with channel ID: %s. %+v", resolveIncidentActionLogTag, cmd.ChannelID, err)) + return fmt.Errorf("failed to fetch incident entity with channel ID: %s", cmd.ChannelID) + } + if incidentEntity == nil { + logger.Error(fmt.Sprintf("%s no entry found for incident with channel ID: %s in DB", resolveIncidentActionLogTag, cmd.ChannelID)) + return genericBackendError + } + + incidentStatusEntity, _ := appcontext.GetIncidentRepo().FindIncidentStatusByName(incident.Resolved) + + tags, err := appcontext.GetTagRepo().FindTagsByTeamId(incidentEntity.TeamId) + + //check if tags are required to be set + var flag = true + if tags != nil { + for _, t := range *tags { + if t.Optional == false { + incidentTag, err := appcontext.GetIncidentRepo().GetIncidentTagByTagId(incidentEntity.ID, t.Id) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to get the incident tag for incidentId: %d", resolveIncidentActionLogTag, incidentEntity.ID)) + return genericBackendError + } + if nil == incidentTag { + flag = false + break + } + if t.Type == "free_text" { + if incidentTag.FreeTextValue == nil || len(*incidentTag.FreeTextValue) == 0 { + flag = false + break + } + + } else { + if incidentTag.TagValueIds == nil || len(incidentTag.TagValueIds) == 0 { + flag = false + break + } + } + + } + } + } else { + logger.Info(fmt.Sprintf("%s tags not required for team id: %v and incident id: %v", resolveIncidentActionLogTag, incidentEntity.TeamId, incidentEntity.ID)) + } + + //check if all tags are set + if flag == true { + now := time.Now() + incidentEntity.Status = incidentStatusEntity.ID + incidentEntity.EndTime = &now + + err = appcontext.GetIncidentRepo().UpdateIncident(incidentEntity) + if err != nil { + logger.Error("failed to update incident to resolve state", + zap.String("channel", cmd.ChannelID), + zap.String("user_id", cmd.UserID), zap.Error(err)) + return fmt.Errorf("failed to update incident status") + } + + logger.Info("successfully resolved the incident", + zap.String("channel", cmd.ChannelID), + zap.String("user_id", cmd.UserID)) + msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `houston set status to %s`", cmd.UserID, + incident.Resolved), false) + _, _, errMessage := action.socketModeClient.PostMessage(cmd.ChannelID, msgOption) + if errMessage != nil { + logger.Error("post response failed for ResolveIncident", zap.Error(errMessage)) + return fmt.Errorf("incident is resolved but failed to post response to slack channel") + } + msgUpdate := NewIncidentChannelMessageUpdateAction( + action.socketModeClient, + appcontext.GetIncidentRepo(), + appcontext.GetTeamRepo(), + appcontext.GetSeverityRepo(), + ) + msgUpdate.ProcessAction(incidentEntity.SlackChannel) + //ToDo() Delete Conference event if exists and if incident is resolved + + go func() { + if incidentEntity.SeverityId != incident.Sev0Id && incidentEntity.SeverityId != incident.Sev1Id { + postErr := util.PostArchivingTimeToIncidentChannel(cmd.ChannelID, incident.Resolved, action.socketModeClient) + if postErr != nil { + logger.Error("failed to post archiving time to incident channel", zap.String("channel id", cmd.ChannelID), zap.Error(err)) + } + } + if viper.GetBool("RCA_GENERATION_ENABLED") { + err = action.rcaService.SendConversationDataForGeneratingRCA(incidentEntity.ID, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("failed to generate rca for incident id: %d of channel id: %s", incidentEntity.ID, cmd.ChannelID), zap.Error(err)) + _, _, errMessage := action.socketModeClient.PostMessage(cmd.ChannelID, slack.MsgOptionText("`Some issue occurred while generating RCA`", false)) + if errMessage != nil { + logger.Error("post response failed for rca failure message", zap.Error(errMessage)) + } + } else { + _, _, errMessage := action.socketModeClient.PostMessage(cmd.ChannelID, slack.MsgOptionText("System RCA generation is in progress and might take 2 to 4 minutes.", false)) + if errMessage != nil { + logger.Error("post response failed for rca generated message", zap.Error(errMessage)) + } + } + } + }() + } else { + msgOption := slack.MsgOptionText(fmt.Sprintf("`Please set tag value`"), false) + _, errMessage := action.socketModeClient.PostEphemeral(cmd.ChannelID, cmd.UserID, msgOption) + if errMessage != nil { + logger.Error("post response failed for ResolveIncident", zap.Error(errMessage)) + return fmt.Errorf("`Please set tag value`") + } + + } + + return nil + }) +} diff --git a/internal/processor/action/set_description_command_action.go b/internal/processor/action/set_description_command_action.go new file mode 100644 index 0000000..77d4794 --- /dev/null +++ b/internal/processor/action/set_description_command_action.go @@ -0,0 +1,85 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/appcontext" + "houston/internal" + "houston/logger" + "houston/pkg/slackbot" + "strings" + "time" +) + +const setDescriptionActionLogTag = "[set_description_command_action]" + +type SetDescriptionCommandAction struct { + socketModeClient *socketmode.Client + slackBot *slackbot.Client +} + +func NewSetDescriptionCommandAction( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *SetDescriptionCommandAction { + return &SetDescriptionCommandAction{ + socketModeClient: socketModeClient, + slackBot: slackBot, + } +} + +func (action *SetDescriptionCommandAction) PerformAction(evt *socketmode.Event) { + cmd, ok := evt.Data.(slack.SlashCommand) + logger.Info("processing houston command", zap.Any("payload", cmd)) + if !ok { + logger.Error("event data to slash command conversion failed", zap.Any("data", evt)) + return + } + + err := action.setDescription(cmd, strings.TrimSpace(cmd.Text[len(internal.SetDescriptionParam):])) + if err != nil { + err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", setDescriptionActionLogTag, err)) + } + } + + action.socketModeClient.Ack(*evt.Request) +} + +// todo: this method has to be removed and usage has to be replaced with update incident V2 once update incident refactor goes live. +func (action *SetDescriptionCommandAction) setDescription(cmd slack.SlashCommand, description string) error { + logger.Info(fmt.Sprintf("%s received request to update the description to %s", setDescriptionActionLogTag, description)) + + return executeForHoustonChannel(cmd, func() error { + incidentEntity, err := appcontext.GetIncidentService().GetIncidentByChannelID(cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch incident entity with channel ID: %s. %+v", setDescriptionActionLogTag, cmd.ChannelID, err)) + return genericBackendError + } + if incidentEntity == nil { + logger.Error(fmt.Sprintf("%s no entry found for incident with channel ID: %s in DB", setDescriptionActionLogTag, cmd.ChannelID)) + return genericBackendError + } + + incidentEntity.Description = description + incidentEntity.UpdatedBy = cmd.UserID + incidentEntity.UpdatedAt = time.Now() + err = appcontext.GetIncidentRepo().UpdateIncident(incidentEntity) + if err != nil { + logger.Error("IncidentUpdateDescription error", + zap.String("incident_slack_channel_id", cmd.ChannelID), zap.String("channel", incidentEntity.IncidentName), + zap.String("user_id", cmd.UserID), zap.Error(err)) + return fmt.Errorf("failed to update incident entity") + } + msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `houston set description to %s`", cmd.UserID, incidentEntity.Description), false) + _, _, errMessage := action.socketModeClient.PostMessage(cmd.ChannelID, msgOption) + if errMessage != nil { + logger.Error("post response failed for IncidentUpdateDescription", zap.Error(errMessage)) + } + + return nil + }) +} diff --git a/internal/processor/action/set_severity_command_action.go b/internal/processor/action/set_severity_command_action.go new file mode 100644 index 0000000..67d95af --- /dev/null +++ b/internal/processor/action/set_severity_command_action.go @@ -0,0 +1,146 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/appcontext" + incidentHelper "houston/common/util" + "houston/internal" + "houston/logger" + "houston/pkg/slackbot" + "strings" + "time" +) + +const setSeverityActionLogTag = "[set_severity_command_action]" + +type SetSeverityCommandAction struct { + socketModeClient *socketmode.Client + slackBot *slackbot.Client +} + +func NewSetSeverityCommandAction( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *SetSeverityCommandAction { + return &SetSeverityCommandAction{ + socketModeClient: socketModeClient, + slackBot: slackBot, + } +} + +func (action *SetSeverityCommandAction) PerformAction(evt *socketmode.Event) { + cmd, ok := evt.Data.(slack.SlashCommand) + logger.Info("processing houston command", zap.Any("payload", cmd)) + if !ok { + logger.Error("event data to slash command conversion failed", zap.Any("data", evt)) + return + } + + err := action.setSeverity(cmd, strings.TrimSpace(cmd.Text[len(internal.SetSeverityParam):])) + if err != nil { + err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", resolveIncidentActionLogTag, err)) + } + } + + action.socketModeClient.Ack(*evt.Request) +} + +// todo: this method has to be removed and usage has to be replaced with update incident V2 once update incident refactor goes live. +func (action *SetSeverityCommandAction) setSeverity(cmd slack.SlashCommand, severity string) error { + logger.Info(fmt.Sprintf("%s received request to update the severity to %s", setSeverityActionLogTag, severity)) + return executeForHoustonChannel(cmd, func() error { + incidentEntity, err := appcontext.GetIncidentService().GetIncidentByChannelID(cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch incident entity with channel ID: %s. %+v", setSeverityActionLogTag, cmd.ChannelID, err)) + return genericBackendError + } + if incidentEntity == nil { + logger.Error(fmt.Sprintf("%s no entry found for incident with channel ID: %s in DB", setSeverityActionLogTag, cmd.ChannelID)) + return genericBackendError + } + severityEntity, err := appcontext.GetSeverityRepo().FindSeverityByName(severity) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch severity entity for: %s. %+v", setSeverityActionLogTag, severity, err)) + return genericBackendError + } + if severityEntity == nil { + logger.Error(fmt.Sprintf("%s no DB entity found for %s. %+v", setSeverityActionLogTag, severity, err)) + return fmt.Errorf("%s is not a valid severity", severity) + } + incidentEntity.SeverityId = severityEntity.ID + incidentEntity.UpdatedBy = cmd.UserID + incidentEntity.SeverityTat = time.Now().AddDate(0, 0, severityEntity.Sla) + err = appcontext.GetIncidentRepo().UpdateIncident(incidentEntity) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to update severity for incident: %s. %+v", setSeverityActionLogTag, incidentEntity.IncidentName, err)) + return fmt.Errorf("failed to update severity for incident: %s", incidentEntity.IncidentName) + } + + teamEntity, err := appcontext.GetTeamRepo().FindTeamById(incidentEntity.TeamId) + if err != nil { + logger.Error(fmt.Sprintf("%s error in finding team entity by team name %d. %+v", setSeverityActionLogTag, incidentEntity.TeamId, err)) + return genericBackendError + } + if teamEntity == nil { + logger.Error(fmt.Sprintf("%s invalid team name %d. No entity found in DB", setSeverityActionLogTag, incidentEntity.TeamId)) + return genericBackendError + } + + for _, o := range severityEntity.SlackUserIds { + action.slackBot.InviteUsersToConversation(cmd.ChannelID, o) + } + go func() { + msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `set severity to %s (%s)`", cmd.UserID, severityEntity.Name, severityEntity.Description), false) + _, _, errMessage := action.socketModeClient.PostMessage(cmd.ChannelID, msgOption) + if errMessage != nil { + logger.Error("post response failed for IncidentUpdateSeverity", zap.Error(errMessage)) + return + } + + txt := fmt.Sprintf("set the channel topic: *%s ยท %s (%s) %s* | %s", teamEntity.Name, severityEntity.Name, severityEntity.Description, incidentEntity.IncidentName, incidentEntity.Title) + att := slack.Attachment{ + Text: txt, + Color: "#808080", // Grey color code + MarkdownIn: []string{"txt"}, // Define which fields support markdown + } + _, _, errMessage = action.socketModeClient.PostMessage(cmd.ChannelID, slack.MsgOptionAttachments(att)) + if errMessage != nil { + logger.Error(fmt.Sprintf("%s post response failed for IncidentUpdateType. %+v", setSeverityActionLogTag, zap.Error(errMessage))) + return + } + + topic := fmt.Sprintf("%s-%s(%s) Incident-%d | %s", + teamEntity.Name, severityEntity.Name, severityEntity.Description, incidentEntity.ID, incidentEntity.Title, + ) + action.slackBot.SetChannelTopic(cmd.ChannelID, topic) + }() + + incidentHelper.TagPseOrDevOncallToIncident( + cmd.ChannelID, + severityEntity, + teamEntity, + action.slackBot, + action.socketModeClient, + ) + + err = incidentHelper.AssignResponderToIncident( + appcontext.GetIncidentRepo(), + incidentEntity, + teamEntity, + severityEntity, + action.socketModeClient, + cmd.UserID, + ) + if err != nil { + logger.Error(fmt.Sprintf("%s Error while assigning responder to the incident %+v", setSeverityActionLogTag, zap.Error(err))) + return fmt.Errorf("severity is set to %s. Failed to assign responder post severity update", severity) + } + + return nil + }) +} diff --git a/internal/processor/action/set_status_command_action.go b/internal/processor/action/set_status_command_action.go new file mode 100644 index 0000000..0523d8c --- /dev/null +++ b/internal/processor/action/set_status_command_action.go @@ -0,0 +1,162 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/appcontext" + "houston/common/util" + "houston/internal" + "houston/logger" + "houston/pkg/slackbot" + "strings" + "time" +) + +const setStatusActionLogTag = "[set_status_command_action]" + +type SetStatusCommandAction struct { + socketModeClient *socketmode.Client + slackBot *slackbot.Client +} + +func NewSetStatusCommandAction( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *SetStatusCommandAction { + return &SetStatusCommandAction{ + socketModeClient: socketModeClient, + slackBot: slackBot, + } +} + +func (action *SetStatusCommandAction) PerformAction(evt *socketmode.Event) { + cmd, ok := evt.Data.(slack.SlashCommand) + logger.Info("processing houston command", zap.Any("payload", cmd)) + if !ok { + logger.Error("event data to slash command conversion failed", zap.Any("data", evt)) + return + } + + err := action.setStatus(cmd, strings.TrimSpace(cmd.Text[len(internal.SetStatusParam):])) + if err != nil { + err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", setStatusActionLogTag, err)) + } + } + + action.socketModeClient.Ack(*evt.Request) +} + +// todo: this method has to be removed and usage has to be replaced with update incident V2 once update incident refactor goes live. +func (action *SetStatusCommandAction) setStatus(cmd slack.SlashCommand, status string) error { + logger.Info(fmt.Sprintf("%s received request to update the status to %s", setStatusActionLogTag, status)) + + return executeForHoustonChannel(cmd, func() error { + if strings.TrimSpace(status) == "Resolved" { + message := "work in progress. Please resolve using `/houston` command for now." + err := appcontext.GetSlackService().PostEphemeralByChannelID(message, cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for invalid slash command param. %+v", setStatusActionLogTag, err)) + } + } else { + incidentEntity, err := appcontext.GetIncidentService().GetIncidentByChannelID(cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch incident entity with channel ID: %s. %+v", setStatusActionLogTag, cmd.ChannelID, err)) + return genericBackendError + } + if incidentEntity == nil { + logger.Error(fmt.Sprintf("%s no entry found for incident with channel ID: %s in DB", setStatusActionLogTag, cmd.ChannelID)) + return genericBackendError + } + + incidentStatusEntity, err := appcontext.GetIncidentRepo().GetIncidentStatusByStatusName(status) + if err != nil { + logger.Error(fmt.Sprintf("%s error in finding incident status for status name %s. %+v", setStatusActionLogTag, status, err)) + return genericBackendError + } + if incidentStatusEntity == nil { + logger.Error(fmt.Sprintf("%s no entity found or status name %s", setStatusActionLogTag, status)) + return fmt.Errorf("%s is not a valid status", status) + } + + statusID := incidentStatusEntity.ID + + tags, err := appcontext.GetTagRepo().FindTagsByTeamId(incidentEntity.TeamId) + if err != nil || tags == nil { + logger.Error(fmt.Sprintf("failure while getting tags for incident id: %v", incidentEntity.ID)) + return genericBackendError + } + var flag = true + for _, t := range *tags { + if t.Optional == false { + incidentTag, err := appcontext.GetIncidentRepo().GetIncidentTagByTagId(incidentEntity.ID, t.Id) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to get the incident tag for incidentId: %d. %+v", setStatusActionLogTag, incidentEntity.ID, err)) + return genericBackendError + } + if nil == incidentTag { + flag = false + break + } + if t.Type == "free_text" { + if incidentTag.FreeTextValue == nil || len(*incidentTag.FreeTextValue) == 0 { + flag = false + break + } + + } else { + if incidentTag.TagValueIds == nil || len(incidentTag.TagValueIds) == 0 { + flag = false + break + } + } + } + } + + incidentEntity.Status = statusID + incidentEntity.UpdatedBy = cmd.UserID + incidentEntity.UpdatedAt = time.Now() + + if incidentStatusEntity.IsTerminalStatus { + if flag == false { + msgOption := slack.MsgOptionText(fmt.Sprintf("`Please set tag value`"), false) + _, errMessage := action.socketModeClient.PostEphemeral(cmd.ChannelID, cmd.UserID, msgOption) + if errMessage != nil { + logger.Error("post response failed for ResolveIncident", zap.Error(errMessage)) + } + } else { + now := time.Now() + incidentEntity.EndTime = &now + } + + } + err = appcontext.GetIncidentRepo().UpdateIncident(incidentEntity) + if err != nil { + logger.Error("UpdateIncident error", + zap.String("incident_slack_channel_id", cmd.ChannelID), zap.String("channel", incidentEntity.IncidentName), + zap.String("user_id", cmd.UserID), zap.Error(err)) + } + + go func() { + errMessage := util.PostIncidentStatusUpdateMessage(cmd.UserID, incidentStatusEntity.Name, cmd.ChannelID, action.socketModeClient) + if errMessage != nil { + logger.Error("post response failed for IncidentUpdateStatus", zap.Error(errMessage)) + return + } + + msgUpdate := NewIncidentChannelMessageUpdateAction( + action.socketModeClient, + appcontext.GetIncidentRepo(), + appcontext.GetTeamRepo(), + appcontext.GetSeverityRepo(), + ) + msgUpdate.ProcessAction(incidentEntity.SlackChannel) + }() + } + + return nil + }) +} diff --git a/internal/processor/action/set_team_command_action.go b/internal/processor/action/set_team_command_action.go new file mode 100644 index 0000000..8ecf748 --- /dev/null +++ b/internal/processor/action/set_team_command_action.go @@ -0,0 +1,146 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/appcontext" + incidentHelper "houston/common/util" + "houston/internal" + "houston/logger" + "houston/pkg/slackbot" + "strings" + "time" +) + +const setTeamActionLogTag = "[set_team_command_action]" + +type SetTeamCommandAction struct { + socketModeClient *socketmode.Client + slackBot *slackbot.Client +} + +func NewSetTeamCommandAction( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *SetTeamCommandAction { + return &SetTeamCommandAction{ + socketModeClient: socketModeClient, + slackBot: slackBot, + } +} + +func (action *SetTeamCommandAction) PerformAction(evt *socketmode.Event) { + cmd, ok := evt.Data.(slack.SlashCommand) + logger.Info("processing houston command", zap.Any("payload", cmd)) + if !ok { + logger.Error("event data to slash command conversion failed", zap.Any("data", evt)) + return + } + + err := action.setTeam(cmd, strings.TrimSpace(cmd.Text[len(internal.SetTeamParam):])) + if err != nil { + err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", setTeamActionLogTag, err)) + } + } + + action.socketModeClient.Ack(*evt.Request) +} + +// todo: this method has to be removed and usage has to be replaced with update incident V2 once update incident refactor goes live. +func (action *SetTeamCommandAction) setTeam(cmd slack.SlashCommand, teamName string) error { + logger.Info(fmt.Sprintf("%s received request to update the team to %s", setTeamActionLogTag, teamName)) + + return executeForHoustonChannel(cmd, func() error { + incidentEntity, err := appcontext.GetIncidentService().GetIncidentByChannelID(cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch incident entity with channel ID: %s. %+v", setTeamActionLogTag, cmd.ChannelID, err)) + return genericBackendError + } + if incidentEntity == nil { + logger.Error(fmt.Sprintf("%s no entry found for incident with channel ID: %s in DB", setTeamActionLogTag, cmd.ChannelID)) + return genericBackendError + } + + teamEntity, err := appcontext.GetTeamRepo().FindTeamByTeamName(teamName) + if err != nil { + logger.Error(fmt.Sprintf("%s error in finding team entity by team name %s. %+v", setTeamActionLogTag, teamName, err)) + return genericBackendError + } + if teamEntity == nil { + logger.Error(fmt.Sprintf("%s invalid team name %s. No entity found in DB", setTeamActionLogTag, teamName)) + return fmt.Errorf("%s is not a valid team name", teamName) + } + + teamId := teamEntity.ID + + incidentEntity.TeamId = teamId + incidentEntity.UpdatedBy = cmd.UserID + incidentEntity.UpdatedAt = time.Now() + err = appcontext.GetIncidentRepo().UpdateIncident(incidentEntity) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to update team ID for incident %s. %+v", setTeamActionLogTag, incidentEntity.IncidentName, err)) + return genericBackendError + } + + go func() { + userIdList := teamEntity.SlackUserIds + for _, user := range userIdList { + action.slackBot.InviteUsersToConversation(cmd.ChannelID, user) + } + + severityEntity, err := appcontext.GetSeverityRepo().FindSeverityById(incidentEntity.SeverityId) + + incidentHelper.TagPseOrDevOncallToIncident( + cmd.ChannelID, + severityEntity, + teamEntity, + action.slackBot, + action.socketModeClient, + ) + + err = incidentHelper.AssignResponderToIncident( + appcontext.GetIncidentRepo(), + incidentEntity, + teamEntity, severityEntity, + action.socketModeClient, + cmd.UserID, + ) + if err != nil { + logger.Error("[Update incident type] Error while assigning responder to the incident ", zap.Error(err)) + } + + if err != nil { + logger.Error("error in fetching severity in incident update type action", zap.String("channel", incidentEntity.SlackChannel), + zap.Uint("incident_id", incidentEntity.ID), zap.Error(err)) + return + } else if severityEntity == nil { + logger.Info("severity not found in incident update type action", zap.String("channel", incidentEntity.SlackChannel), + zap.Uint("incident_id", incidentEntity.ID)) + return + } + errMessage := incidentHelper.PostIncidentTypeUpdateMessage( + cmd.UserID, teamEntity.Name, severityEntity.Name, severityEntity.Description, + incidentEntity.IncidentName, incidentEntity.Title, cmd.ChannelID, action.socketModeClient) + if errMessage != nil { + logger.Error("post response failed for IncidentUpdateType", zap.Error(errMessage)) + return + } + if len(teamEntity.ConfluenceLink) > 1 { + textForConfluence := fmt.Sprintf("*%s* -> <%s| Confluence Page>", teamEntity.Name, teamEntity.ConfluenceLink) + _, _, errMessage := action.socketModeClient.PostMessage(cmd.ChannelID, slack.MsgOptionText(textForConfluence, false)) + if errMessage != nil { + logger.Error("post response failed for IncidentUpdateType Confluence message", zap.Error(errMessage)) + return + } + } + topic := fmt.Sprintf("%s-%s(%s) Incident-%d | %s", teamEntity.Name, severityEntity.Name, severityEntity.Description, incidentEntity.ID, incidentEntity.Title) + action.slackBot.SetChannelTopic(cmd.ChannelID, topic) + }() + + return nil + }) +} diff --git a/internal/processor/action/start_incident_command_action.go b/internal/processor/action/start_incident_command_action.go new file mode 100644 index 0000000..78caea1 --- /dev/null +++ b/internal/processor/action/start_incident_command_action.go @@ -0,0 +1,220 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "github.com/spf13/viper" + "go.uber.org/zap" + "houston/appcontext" + "houston/common/util" + "houston/internal" + "houston/internal/processor/action/view" + "houston/logger" + "houston/pkg/slackbot" + request "houston/service/request" + "strconv" + "strings" +) + +const startIncidentActionLogTag = "[slash_command_action]" + +type StartIncidentCommandAction struct { + socketModeClient *socketmode.Client + slackBot *slackbot.Client +} + +func NewStartIncidentCommandAction( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *StartIncidentCommandAction { + return &StartIncidentCommandAction{ + socketModeClient: socketModeClient, + slackBot: slackBot, + } +} + +func (action *StartIncidentCommandAction) PerformAction(evt *socketmode.Event) { + cmd, ok := evt.Data.(slack.SlashCommand) + logger.Info("processing houston command", zap.Any("payload", cmd)) + if !ok { + logger.Error("event data to slash command conversion failed", zap.Any("data", evt)) + return + } + + err := action.startIncidentWithParams( + cmd, + strings.TrimSpace(cmd.Text[len(internal.StartIncidentParam):]), + ) + if err != nil { + err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", startIncidentActionLogTag, err)) + } + } + + action.socketModeClient.Ack(*evt.Request) +} + +func (action *StartIncidentCommandAction) startIncidentWithParams( + cmd slack.SlashCommand, + params string, +) error { + logger.Info(fmt.Sprintf("%s received command to start incident with params: %s", startIncidentActionLogTag, params)) + return executeForNonHoustonChannel(cmd, func() error { + if len(params) == 0 { + logger.Info(fmt.Sprintf("%s launching view modal to start new incident", startIncidentActionLogTag)) + teams, err := appcontext.GetTeamRepo().GetAllActiveTeams() + if err != nil || teams == nil { + logger.Error(fmt.Sprintf("%s failed to fetch all active teams from DB. %+v", startIncidentActionLogTag, err)) + return genericBackendError + } + + severities, err := appcontext.GetSeverityRepo().GetAllActiveSeverity() + if err != nil || severities == nil { + logger.Error(fmt.Sprintf("%s failed to fetch all severities from DB. %+v", startIncidentActionLogTag, err)) + return genericBackendError + } + _, err = action.socketModeClient.OpenView(cmd.TriggerID, view.GenerateModalRequest(*teams, *severities, cmd.ChannelID)) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to open create new incident view modal: %+v", startIncidentActionLogTag, err)) + return fmt.Errorf("failed to open create new incident view modal") + } + } else { + logger.Info(fmt.Sprintf("%s creating new incident with params: %s", startIncidentActionLogTag, params)) + + createIncidentRequest, err := buildCreateIncidentRequest(params, cmd.UserID) + if err != nil { + return err + } + + createIncidentResponse, err := appcontext.GetIncidentService().CreateIncident(*createIncidentRequest, "SLACK", cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to create incident. %+v", startIncidentActionLogTag, err)) + return fmt.Errorf("failed to create incident") + } + logger.Info(fmt.Sprintf("%s incident created: %+v", startIncidentActionLogTag, createIncidentResponse)) + } + return nil + }) +} + +func buildCreateIncidentRequest(param string, userID string) (*request.CreateIncidentRequestV2, error) { + if !strings.Contains(param, " title ") && strings.Contains(param, " description ") { + return nil, fmt.Errorf("title and description are required. Run `/houston help` for checking valid syntax") + } + firstPart, remainingString := util.SplitUntilWord(param, "title") + severityAndTeamSlice := strings.Split(strings.TrimSpace(firstPart), " ") + if len(severityAndTeamSlice) != 2 { + return nil, fmt.Errorf("enter valid severity and team") + } + + //get valid severity from param + severityName := severityAndTeamSlice[0] + severityEntity, err := appcontext.GetSeverityRepo().FindSeverityByName(severityName) + if err != nil { + logger.Error(fmt.Sprintf("%s error in finding severity entity for severity name: %s. %+v", startIncidentActionLogTag, severityName, err)) + return nil, fmt.Errorf("error in finding severity entity for severity name: %s", severityName) + } + if severityEntity == nil { + return nil, fmt.Errorf("enter a valid severity name") + } + severityID := severityEntity.ID + + //get team from param + teamName := severityAndTeamSlice[1] + teamEntity, err := appcontext.GetTeamRepo().FindTeamByTeamName(teamName) + if err != nil { + logger.Error(fmt.Sprintf("%s error in finding team entity for team name: %s. %+v", startIncidentActionLogTag, teamName, err)) + return nil, fmt.Errorf("error in finding severity entity for severity name: %s", teamName) + } + if teamEntity == nil { + return nil, fmt.Errorf("enter a valid team name") + } + var teamID uint + if teamEntity.Active { + teamID = teamEntity.ID + } else { + return nil, fmt.Errorf("entered team is not active") + } + + //get title and description from param + title, description := util.SplitUntilWord(remainingString, "description") + if len(strings.TrimSpace(title)) == 0 { + return nil, fmt.Errorf("title can not be left empty") + } + + titleMaxLength := viper.GetInt("create-incident.title.max-length") + + if len(strings.TrimSpace(title)) > titleMaxLength { + return nil, fmt.Errorf("title can not be more than %d characters long", titleMaxLength) + } + + if len(strings.TrimSpace(description)) == 0 { + return nil, fmt.Errorf("description can not be left empty") + } + + descriptionMaxLength := viper.GetInt("create-incident.description.max-length") + + if len(strings.TrimSpace(title)) > descriptionMaxLength { + return nil, fmt.Errorf("description can not be more than %d characters long", descriptionMaxLength) + } + + //build request + createIncidentRequest := &request.CreateIncidentRequestV2{ + Title: strings.TrimSpace(title), + Description: strings.TrimSpace(description), + SeverityID: strconv.Itoa(int(severityID)), + TeamID: strconv.Itoa(int(teamID)), + CreatedBy: userID, + } + return createIncidentRequest, nil +} + +func executeForNonHoustonChannel(cmd slack.SlashCommand, fn func() error) error { + isAHoustonChannel, err := appcontext.GetIncidentService().IsHoustonChannel(cmd.ChannelID) + if err != nil { + message := "Failed to check if current channel is a non houston channel in DB. Please try again or run `/houston` command" + err := appcontext.GetSlackService().PostEphemeralByChannelID(message, cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for houston channel check", startIncidentActionLogTag)) + } + } else { + if !isAHoustonChannel { + return fn() + } else { + message := "This action is not allowed from a houston channel" + err := appcontext.GetSlackService().PostEphemeralByChannelID(message, cmd.UserID, false, cmd.ChannelID) + if err != nil { + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for incident creation restriction in a houston channel", startIncidentActionLogTag)) + } + } + } + } + return nil +} + +func executeForHoustonChannel(cmd slack.SlashCommand, fn func() error) error { + isAHoustonChannel, err := appcontext.GetIncidentService().IsHoustonChannel(cmd.ChannelID) + if err != nil { + message := "Failed to check if current channel is a houston channel in DB. Please try again or run `/houston` command" + err := appcontext.GetSlackService().PostEphemeralByChannelID(message, cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for houston channel check", startIncidentActionLogTag)) + } + } else { + if isAHoustonChannel { + return fn() + } else { + message := "This action is not allowed from a non houston channel" + err := appcontext.GetSlackService().PostEphemeralByChannelID(message, cmd.UserID, false, cmd.ChannelID) + if err != nil { + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for incident creation restriction in a houston channel", startIncidentActionLogTag)) + } + } + } + } + return nil +} diff --git a/internal/processor/action/view/incident_description.go b/internal/processor/action/view/incident_description.go index 1ff2161..c61098d 100644 --- a/internal/processor/action/view/incident_description.go +++ b/internal/processor/action/view/incident_description.go @@ -5,7 +5,7 @@ import ( "houston/common/util" ) -func BuildIncidentUpdateDescriptionModal(channel slack.Channel, description string) slack.ModalViewRequest { +func BuildIncidentUpdateDescriptionModal(channelID string, description string) slack.ModalViewRequest { titleText := slack.NewTextBlockObject(slack.PlainTextType, "Set description", false, false) closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false) @@ -35,7 +35,7 @@ func BuildIncidentUpdateDescriptionModal(channel slack.Channel, description stri Close: closeText, Submit: submitText, Blocks: blocks, - PrivateMetadata: channel.ID, + PrivateMetadata: channelID, CallbackID: util.SetIncidentDescriptionSubmit, } diff --git a/internal/processor/action/view/incident_jira_links.go b/internal/processor/action/view/incident_jira_links.go index ee5bf0a..5d50454 100644 --- a/internal/processor/action/view/incident_jira_links.go +++ b/internal/processor/action/view/incident_jira_links.go @@ -6,7 +6,7 @@ import ( "strings" ) -func BuildJiraLinksModal(channel slack.Channel, jiraLinks ...string) slack.ModalViewRequest { +func BuildJiraLinksModal(channelID string, jiraLinks ...string) slack.ModalViewRequest { titleText := slack.NewTextBlockObject(slack.PlainTextType, "Jira link(s)", false, false) closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false) @@ -32,7 +32,7 @@ func BuildJiraLinksModal(channel slack.Channel, jiraLinks ...string) slack.Modal Close: closeText, Submit: submitText, Blocks: blocks, - PrivateMetadata: channel.ID, + PrivateMetadata: channelID, CallbackID: util.SetIncidentJiraLinksSubmit, } diff --git a/internal/processor/action/view/incident_resolution_text.go b/internal/processor/action/view/incident_resolution_text.go index 4eab10d..02fd33e 100644 --- a/internal/processor/action/view/incident_resolution_text.go +++ b/internal/processor/action/view/incident_resolution_text.go @@ -5,7 +5,7 @@ import ( "houston/common/util" ) -func BuildRcaModal(channel slack.Channel, description string) slack.ModalViewRequest { +func BuildRcaModal(channelID string, description string) slack.ModalViewRequest { titleText := slack.NewTextBlockObject(slack.PlainTextType, "Incident RCA", false, false) closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false) @@ -35,7 +35,7 @@ func BuildRcaModal(channel slack.Channel, description string) slack.ModalViewReq Close: closeText, Submit: submitText, Blocks: blocks, - PrivateMetadata: channel.ID, + PrivateMetadata: channelID, CallbackID: util.SetIncidentRcaSubmit, } diff --git a/internal/processor/action/view/incident_section.go b/internal/processor/action/view/incident_section.go index 00fb073..7b42465 100644 --- a/internal/processor/action/view/incident_section.go +++ b/internal/processor/action/view/incident_section.go @@ -35,7 +35,7 @@ func NewIncidentBlock() map[string]interface{} { }, ), slack.NewButtonBlockElement( - "help_commands_button", + util.HelpCommand, "help_commands_button_value", &slack.TextBlockObject{ Type: slack.PlainTextType, diff --git a/internal/processor/action/view/incident_severity.go b/internal/processor/action/view/incident_severity.go index 27c430b..0552a0f 100644 --- a/internal/processor/action/view/incident_severity.go +++ b/internal/processor/action/view/incident_severity.go @@ -9,7 +9,7 @@ import ( "github.com/slack-go/slack" ) -func BuildIncidentUpdateSeverityModal(channel slack.Channel, incidentSeverity []severity.SeverityEntity) slack.ModalViewRequest { +func BuildIncidentUpdateSeverityModal(channelID string, incidentSeverity []severity.SeverityEntity) slack.ModalViewRequest { titleText := slack.NewTextBlockObject(slack.PlainTextType, "Set severity of incident", false, false) closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false) @@ -36,7 +36,7 @@ func BuildIncidentUpdateSeverityModal(channel slack.Channel, incidentSeverity [] Close: closeText, Submit: submitText, Blocks: blocks, - PrivateMetadata: channel.ID, + PrivateMetadata: channelID, CallbackID: util.SetIncidentSeveritySubmit, } diff --git a/internal/processor/action/view/incident_status_update_modal.go b/internal/processor/action/view/incident_status_update_modal.go index 3d053e7..5fa3bb2 100644 --- a/internal/processor/action/view/incident_status_update_modal.go +++ b/internal/processor/action/view/incident_status_update_modal.go @@ -9,7 +9,7 @@ import ( "github.com/slack-go/slack" ) -func BuildIncidentUpdateStatusModal(statuses []incident.IncidentStatusEntity, channel slack.Channel) slack.ModalViewRequest { +func BuildIncidentUpdateStatusModal(statuses []incident.IncidentStatusEntity, channelID string) slack.ModalViewRequest { titleText := slack.NewTextBlockObject(slack.PlainTextType, "Set status of incident", false, false) closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false) @@ -36,7 +36,7 @@ func BuildIncidentUpdateStatusModal(statuses []incident.IncidentStatusEntity, ch Close: closeText, Submit: submitText, Blocks: blocks, - PrivateMetadata: channel.ID, + PrivateMetadata: channelID, CallbackID: util.SetIncidentStatusSubmit, } diff --git a/internal/processor/action/view/incident_update_type.go b/internal/processor/action/view/incident_update_type.go index ad1e4f4..cce615d 100644 --- a/internal/processor/action/view/incident_update_type.go +++ b/internal/processor/action/view/incident_update_type.go @@ -9,7 +9,7 @@ import ( "github.com/slack-go/slack" ) -func BuildIncidentUpdateTypeModal(channel slack.Channel, teams []team.TeamEntity) slack.ModalViewRequest { +func BuildIncidentUpdateTypeModal(channelID string, teams []team.TeamEntity) slack.ModalViewRequest { titleText := slack.NewTextBlockObject(slack.PlainTextType, "Set type of incident", false, false) closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false) @@ -36,7 +36,7 @@ func BuildIncidentUpdateTypeModal(channel slack.Channel, teams []team.TeamEntity Close: closeText, Submit: submitText, Blocks: blocks, - PrivateMetadata: channel.ID, + PrivateMetadata: channelID, CallbackID: util.SetIncidentTypeSubmit, } diff --git a/internal/processor/event_type_interactive_processor.go b/internal/processor/event_type_interactive_processor.go index ae583ac..c00adca 100644 --- a/internal/processor/event_type_interactive_processor.go +++ b/internal/processor/event_type_interactive_processor.go @@ -2,6 +2,7 @@ package processor import ( "fmt" + "github.com/spf13/viper" "gorm.io/gorm" "houston/common/util" "houston/internal/processor/action" @@ -28,6 +29,7 @@ type BlockActionProcessor struct { socketModeClient *socketmode.Client startIncidentBlockAction *action.StartIncidentBlockAction showIncidentsAction *action.ShowIncidentsAction + helpCommandsAction *action.HelpCommandsAction assignIncidentAction *action.AssignIncidentAction incidentResolveAction *action.ResolveIncidentAction incidentUpdateAction *action.UpdateIncidentAction @@ -61,6 +63,7 @@ func NewBlockActionProcessor( startIncidentBlockAction: action.NewStartIncidentBlockAction(socketModeClient, teamService, severityService), showIncidentsAction: action.ShowIncidentsBlockAction(socketModeClient, teamService), + helpCommandsAction: action.NewHelpCommandsAction(socketModeClient), assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, incidentRepository), incidentResolveAction: action.NewIncidentResolveProcessor(socketModeClient, incidentRepository, tagService, teamService, severityService, rcaService), @@ -108,6 +111,10 @@ func (bap *BlockActionProcessor) ProcessCommand(callback slack.InteractionCallba { bap.showIncidentsAction.ProcessAction(callback, request) } + case util.HelpCommand: + { + bap.helpCommandsAction.ProcessAction(callback.User.ID, callback.Channel.ID, request) + } case util.Incident: { bap.processIncidentCommands(callback, request) @@ -286,7 +293,7 @@ func (vsp *ViewSubmissionProcessor) ProcessCommand(callback slack.InteractionCal switch callbackId { case util.StartIncidentSubmit: { - createIncidentV2Enabled := true + createIncidentV2Enabled := viper.GetBool("CREATE_INCIDENT_V2_ENABLED") if createIncidentV2Enabled { vsp.createIncidentAction.CreateIncidentModalCommandProcessingV2(callback, request) } else { diff --git a/internal/processor/help_command_processor.go b/internal/processor/help_command_processor.go new file mode 100644 index 0000000..9709087 --- /dev/null +++ b/internal/processor/help_command_processor.go @@ -0,0 +1,39 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" +) + +type HelpCommandProcessor struct { + socketModeClient *socketmode.Client + helpCommandAction *action.HelpCommandsAction +} + +const helpCommandProcessorLogTag = "[help_command_processor]" + +func NewHelpCommandProcessor( + socketModeClient *socketmode.Client, +) *HelpCommandProcessor { + return &HelpCommandProcessor{ + socketModeClient: socketModeClient, + helpCommandAction: action.NewHelpCommandsAction(socketModeClient), + } +} + +func (processor *HelpCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", helpCommandProcessorLogTag, r.(error))) + } + }() + cmd, ok := event.Data.(slack.SlashCommand) + if !ok { + logger.Error(fmt.Sprintf("%s failed to convert event data into slash command for data: %+v", helpCommandProcessorLogTag, event.Data)) + return + } + processor.helpCommandAction.ProcessAction(cmd.UserID, cmd.ChannelID, event.Request) +} diff --git a/internal/processor/houston_command_processor.go b/internal/processor/houston_command_processor.go new file mode 100644 index 0000000..13da0c0 --- /dev/null +++ b/internal/processor/houston_command_processor.go @@ -0,0 +1,38 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" + "houston/pkg/slackbot" + "houston/service/rca" +) + +type HoustonCommandProcessor struct { + socketModeClient *socketmode.Client + houstonCommandAction *action.HoustonCommandAction +} + +const houstonCommandProcessorLogTag = "[houston_command_processor]" + +func NewHoustonCommandProcessor( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, + rcaService *rca.RcaService, +) *HoustonCommandProcessor { + return &HoustonCommandProcessor{ + socketModeClient: socketModeClient, + houstonCommandAction: action.NewHoustonCommandAction(socketModeClient, slackBot, rcaService), + } +} + +func (processor *HoustonCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", houstonCommandProcessorLogTag, r.(error))) + } + }() + + processor.houstonCommandAction.PerformAction(event) +} diff --git a/internal/processor/open_set_description_view_modal_command_processor.go b/internal/processor/open_set_description_view_modal_command_processor.go new file mode 100644 index 0000000..270b42f --- /dev/null +++ b/internal/processor/open_set_description_view_modal_command_processor.go @@ -0,0 +1,36 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" + "houston/pkg/slackbot" +) + +type OpenSetDescriptionViewModalCommandProcessor struct { + socketModeClient *socketmode.Client + openSetDescriptionViewModalCommandAction *action.OpenSetDescriptionViewModalCommandAction +} + +const openSetDescriptionViewModalCommandProcessorLogTag = "[open_set_description_view_modal_command_processor]" + +func NewOpenSetDescriptionViewModalCommandProcessor( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *OpenSetDescriptionViewModalCommandProcessor { + return &OpenSetDescriptionViewModalCommandProcessor{ + socketModeClient: socketModeClient, + openSetDescriptionViewModalCommandAction: action.NewOpenSetDescriptionViewModalCommandAction(socketModeClient, slackBot), + } +} + +func (processor *OpenSetDescriptionViewModalCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", openSetDescriptionViewModalCommandProcessorLogTag, r.(error))) + } + }() + + processor.openSetDescriptionViewModalCommandAction.PerformAction(event) +} diff --git a/internal/processor/open_set_rca_view_modal_command_processor.go b/internal/processor/open_set_rca_view_modal_command_processor.go new file mode 100644 index 0000000..d007ebe --- /dev/null +++ b/internal/processor/open_set_rca_view_modal_command_processor.go @@ -0,0 +1,36 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" + "houston/pkg/slackbot" +) + +type OpenFillRCAViewModalCommandProcessor struct { + socketModeClient *socketmode.Client + openSetRCAViewModalCommandAction *action.OpenFillRCAViewModalCommandAction +} + +const openSetRCAViewModalCommandProcessorLogTag = "[open_set_rca_view_modal_command_processor]" + +func NewOpenFillRCAViewModalCommandProcessor( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *OpenFillRCAViewModalCommandProcessor { + return &OpenFillRCAViewModalCommandProcessor{ + socketModeClient: socketModeClient, + openSetRCAViewModalCommandAction: action.NewOpenFillRCAViewModalCommandAction(socketModeClient, slackBot), + } +} + +func (processor *OpenFillRCAViewModalCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", openSetRCAViewModalCommandProcessorLogTag, r.(error))) + } + }() + + processor.openSetRCAViewModalCommandAction.PerformAction(event) +} diff --git a/internal/processor/open_set_severity_view_modal_command_processor.go b/internal/processor/open_set_severity_view_modal_command_processor.go new file mode 100644 index 0000000..cb4ae5d --- /dev/null +++ b/internal/processor/open_set_severity_view_modal_command_processor.go @@ -0,0 +1,36 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" + "houston/pkg/slackbot" +) + +type OpenSetSeverityViewModalCommandProcessor struct { + socketModeClient *socketmode.Client + openSetSeverityViewModalCommandAction *action.OpenSetSeverityViewModalCommandAction +} + +const openSetSeverityViewModalCommandProcessorLogTag = "[open_set_severity_view_modal_command_processor]" + +func NewOpenSetSeverityViewModalCommandProcessor( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *OpenSetSeverityViewModalCommandProcessor { + return &OpenSetSeverityViewModalCommandProcessor{ + socketModeClient: socketModeClient, + openSetSeverityViewModalCommandAction: action.NewOpenSetSeverityViewModalCommandAction(socketModeClient, slackBot), + } +} + +func (processor *OpenSetSeverityViewModalCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", openSetSeverityViewModalCommandProcessorLogTag, r.(error))) + } + }() + + processor.openSetSeverityViewModalCommandAction.PerformAction(event) +} diff --git a/internal/processor/open_set_status_view_modal_command_processor.go b/internal/processor/open_set_status_view_modal_command_processor.go new file mode 100644 index 0000000..39f3b78 --- /dev/null +++ b/internal/processor/open_set_status_view_modal_command_processor.go @@ -0,0 +1,36 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" + "houston/pkg/slackbot" +) + +type OpenSetStatusViewModalCommandProcessor struct { + socketModeClient *socketmode.Client + openSetStatusViewModalCommandAction *action.OpenSetStatusViewModalCommandAction +} + +const openStatusViewModalCommandProcessorLogTag = "[open_set_status_view_modal_command_processor]" + +func NewOpenSetStatusViewModalCommandProcessor( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *OpenSetStatusViewModalCommandProcessor { + return &OpenSetStatusViewModalCommandProcessor{ + socketModeClient: socketModeClient, + openSetStatusViewModalCommandAction: action.NewOpenSetStatusViewModalCommandAction(socketModeClient, slackBot), + } +} + +func (processor *OpenSetStatusViewModalCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", openStatusViewModalCommandProcessorLogTag, r.(error))) + } + }() + + processor.openSetStatusViewModalCommandAction.PerformAction(event) +} diff --git a/internal/processor/open_set_team_view_modal_command_processor.go b/internal/processor/open_set_team_view_modal_command_processor.go new file mode 100644 index 0000000..4d8d719 --- /dev/null +++ b/internal/processor/open_set_team_view_modal_command_processor.go @@ -0,0 +1,36 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" + "houston/pkg/slackbot" +) + +type OpenSetTeamViewModalCommandProcessor struct { + socketModeClient *socketmode.Client + openSetTeamViewModalCommandAction *action.OpenSetTeamViewModalCommandAction +} + +const openSetTeamViewModalCommandProcessorLogTag = "[open_set_team_view_modal_command_processor]" + +func NewOpenSetTeamViewModalCommandProcessor( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *OpenSetTeamViewModalCommandProcessor { + return &OpenSetTeamViewModalCommandProcessor{ + socketModeClient: socketModeClient, + openSetTeamViewModalCommandAction: action.NewOpenSetTeamViewModalCommandAction(socketModeClient, slackBot), + } +} + +func (processor *OpenSetTeamViewModalCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", openSetTeamViewModalCommandProcessorLogTag, r.(error))) + } + }() + + processor.openSetTeamViewModalCommandAction.PerformAction(event) +} diff --git a/internal/processor/resolve_incident_command_processor.go b/internal/processor/resolve_incident_command_processor.go new file mode 100644 index 0000000..7cfcc6c --- /dev/null +++ b/internal/processor/resolve_incident_command_processor.go @@ -0,0 +1,39 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" + "houston/pkg/slackbot" + "houston/service/rca" +) + +type ResolveIncidentCommandProcessor struct { + socketModeClient *socketmode.Client + resolveIncidentCommandAction *action.ResolveIncidentCommandAction + rcaService *rca.RcaService +} + +const ResolveIncidentCommandProcessorLogTag = "[start_incident_command_processor]" + +func NewResolveIncidentCommandProcessor( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, + rcaService *rca.RcaService, +) *ResolveIncidentCommandProcessor { + return &ResolveIncidentCommandProcessor{ + socketModeClient: socketModeClient, + resolveIncidentCommandAction: action.NewResolveIncidentCommandAction(socketModeClient, slackBot, rcaService), + } +} + +func (processor *ResolveIncidentCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", ResolveIncidentCommandProcessorLogTag, r.(error))) + } + }() + + processor.resolveIncidentCommandAction.PerformAction(event) +} diff --git a/internal/processor/set_description_command_processor.go b/internal/processor/set_description_command_processor.go new file mode 100644 index 0000000..51032d9 --- /dev/null +++ b/internal/processor/set_description_command_processor.go @@ -0,0 +1,36 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" + "houston/pkg/slackbot" +) + +type SetDescriptionCommandProcessor struct { + socketModeClient *socketmode.Client + setDescriptionCommandAction *action.SetDescriptionCommandAction +} + +const setDescriptionCommandProcessorLogTag = "[set_description_command_processor]" + +func NewSetDescriptionCommandProcessor( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *SetDescriptionCommandProcessor { + return &SetDescriptionCommandProcessor{ + socketModeClient: socketModeClient, + setDescriptionCommandAction: action.NewSetDescriptionCommandAction(socketModeClient, slackBot), + } +} + +func (processor *SetDescriptionCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", setDescriptionCommandProcessorLogTag, r.(error))) + } + }() + + processor.setDescriptionCommandAction.PerformAction(event) +} diff --git a/internal/processor/set_severity_command_processor.go b/internal/processor/set_severity_command_processor.go new file mode 100644 index 0000000..b6a139a --- /dev/null +++ b/internal/processor/set_severity_command_processor.go @@ -0,0 +1,36 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" + "houston/pkg/slackbot" +) + +type SetSeverityCommandProcessor struct { + socketModeClient *socketmode.Client + setSeverityCommandAction *action.SetSeverityCommandAction +} + +const SetSeverityCommandProcessorLogTag = "[start_incident_command_processor]" + +func NewSetSeverityCommandProcessor( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *SetSeverityCommandProcessor { + return &SetSeverityCommandProcessor{ + socketModeClient: socketModeClient, + setSeverityCommandAction: action.NewSetSeverityCommandAction(socketModeClient, slackBot), + } +} + +func (processor *SetSeverityCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", StartIncidentCommandProcessorLogTag, r.(error))) + } + }() + + processor.setSeverityCommandAction.PerformAction(event) +} diff --git a/internal/processor/set_status_command_processor.go b/internal/processor/set_status_command_processor.go new file mode 100644 index 0000000..9045c52 --- /dev/null +++ b/internal/processor/set_status_command_processor.go @@ -0,0 +1,36 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" + "houston/pkg/slackbot" +) + +type SetStatusCommandProcessor struct { + socketModeClient *socketmode.Client + setStatusCommandAction *action.SetStatusCommandAction +} + +const setStatusCommandProcessorLogTag = "[set_status_command_processor]" + +func NewSetStatusCommandProcessor( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *SetStatusCommandProcessor { + return &SetStatusCommandProcessor{ + socketModeClient: socketModeClient, + setStatusCommandAction: action.NewSetStatusCommandAction(socketModeClient, slackBot), + } +} + +func (processor *SetStatusCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", setStatusCommandProcessorLogTag, r.(error))) + } + }() + + processor.setStatusCommandAction.PerformAction(event) +} diff --git a/internal/processor/set_team_command_processor.go b/internal/processor/set_team_command_processor.go new file mode 100644 index 0000000..0ba9754 --- /dev/null +++ b/internal/processor/set_team_command_processor.go @@ -0,0 +1,36 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" + "houston/pkg/slackbot" +) + +type SetTeamCommandProcessor struct { + socketModeClient *socketmode.Client + setTeamCommandAction *action.SetTeamCommandAction +} + +const setTeamCommandProcessorLogTag = "[set_team_command_processor]" + +func NewSetTeamCommandProcessor( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *SetTeamCommandProcessor { + return &SetTeamCommandProcessor{ + socketModeClient: socketModeClient, + setTeamCommandAction: action.NewSetTeamCommandAction(socketModeClient, slackBot), + } +} + +func (processor *SetTeamCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", setTeamCommandProcessorLogTag, r.(error))) + } + }() + + processor.setTeamCommandAction.PerformAction(event) +} diff --git a/internal/processor/slash_command_processor.go b/internal/processor/slash_command_processor.go index 5f44c3c..ece8cc2 100644 --- a/internal/processor/slash_command_processor.go +++ b/internal/processor/slash_command_processor.go @@ -9,8 +9,8 @@ import ( "houston/internal/diagnostic/models" "houston/internal/processor/action" "houston/logger" - "houston/model/incident" "houston/pkg/slackbot" + "houston/service/rca" "strings" ) @@ -19,9 +19,8 @@ type CommandProcessor interface { } type SlashCommandProcessor struct { - logger *zap.Logger socketModeClient *socketmode.Client - slashCommandAction *action.SlashCommandAction + slashCommandAction *action.HoustonCommandAction } type DiagnosticCommandProcessor struct { @@ -43,11 +42,13 @@ func NewDiagnosticCommandProcessor(socketModeClient *socketmode.Client, reposito } func NewSlashCommandProcessor( - socketModeClient *socketmode.Client, incidentService *incident.Repository, slackBot *slackbot.Client, + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, + rcaService *rca.RcaService, ) *SlashCommandProcessor { return &SlashCommandProcessor{ socketModeClient: socketModeClient, - slashCommandAction: action.NewSlashCommandAction(incidentService, socketModeClient, slackBot), + slashCommandAction: action.NewHoustonCommandAction(socketModeClient, slackBot, rcaService), } } diff --git a/internal/processor/start_incident_command_processor.go b/internal/processor/start_incident_command_processor.go new file mode 100644 index 0000000..07acebf --- /dev/null +++ b/internal/processor/start_incident_command_processor.go @@ -0,0 +1,36 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "houston/internal/processor/action" + "houston/logger" + "houston/pkg/slackbot" +) + +type StartIncidentCommandProcessor struct { + socketModeClient *socketmode.Client + startIncidentCommandAction *action.StartIncidentCommandAction +} + +const StartIncidentCommandProcessorLogTag = "[start_incident_command_processor]" + +func NewStartIncidentCommandProcessor( + socketModeClient *socketmode.Client, + slackBot *slackbot.Client, +) *StartIncidentCommandProcessor { + return &StartIncidentCommandProcessor{ + socketModeClient: socketModeClient, + startIncidentCommandAction: action.NewStartIncidentCommandAction(socketModeClient, slackBot), + } +} + +func (processor *StartIncidentCommandProcessor) ProcessSlashCommand(event *socketmode.Event) { + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("%s Exception occurred: %+v", StartIncidentCommandProcessorLogTag, r.(error))) + } + }() + + processor.startIncidentCommandAction.PerformAction(event) +} diff --git a/internal/resolver/houston_command_resolver.go b/internal/resolver/houston_command_resolver.go new file mode 100644 index 0000000..4a7fe60 --- /dev/null +++ b/internal/resolver/houston_command_resolver.go @@ -0,0 +1,138 @@ +package resolver + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "houston/appcontext" + "houston/internal" + "houston/internal/processor" + "houston/logger" + "houston/pkg/slackbot" + "houston/service/rca" + "strings" +) + +type HoustonCommandResolver struct { + diagnosticCommandProcessor *processor.DiagnosticCommandProcessor + socketModeClient *socketmode.Client + slackBotClient *slackbot.Client + rcaService *rca.RcaService +} + +func NewHoustonCommandResolver( + diagnosticCommandProcessor *processor.DiagnosticCommandProcessor, + socketModeClient *socketmode.Client, + slackBotClient *slackbot.Client, + rcaService *rca.RcaService, +) *HoustonCommandResolver { + return &HoustonCommandResolver{ + diagnosticCommandProcessor: diagnosticCommandProcessor, + socketModeClient: socketModeClient, + slackBotClient: slackBotClient, + rcaService: rcaService, + } +} + +const logTag = "[houston_command_resolver]" + +func (resolver *HoustonCommandResolver) Resolve(evt *socketmode.Event) processor.CommandProcessor { + cmd, _ := evt.Data.(slack.SlashCommand) + if resolver.diagnosticCommandProcessor.ShouldProcessCommand(cmd.Text) { + resolver.diagnosticCommandProcessor.ProcessSlashCommand(evt) + } + logger.Info(fmt.Sprintf("%s received slash command [%s] from channel [%s] by user [%s]", logTag, fmt.Sprintf("%s %s", cmd.Command, cmd.Text), cmd.ChannelName, cmd.UserName)) + + params := strings.ToLower(cmd.Text) + + var commandProcessor processor.CommandProcessor + + if strings.TrimSpace(params) == "" { + hcp := processor.NewHoustonCommandProcessor(resolver.socketModeClient, resolver.slackBotClient, resolver.rcaService) + commandProcessor = hcp + } else { + switch { + case strings.HasPrefix(params, string(internal.StartIncidentParam)): + commandProcessor = processor.NewStartIncidentCommandProcessor( + resolver.socketModeClient, + resolver.slackBotClient, + ) + + case strings.HasPrefix(params, internal.OpenSeverityModalParam): + commandProcessor = processor.NewOpenSetSeverityViewModalCommandProcessor( + resolver.socketModeClient, + resolver.slackBotClient, + ) + + case strings.HasPrefix(params, internal.SetSeverityParam): + commandProcessor = processor.NewSetSeverityCommandProcessor( + resolver.socketModeClient, + resolver.slackBotClient, + ) + + case strings.HasPrefix(params, internal.OpenTeamModalParam): + commandProcessor = processor.NewOpenSetTeamViewModalCommandProcessor( + resolver.socketModeClient, + resolver.slackBotClient, + ) + + case strings.HasPrefix(params, internal.SetTeamParam): + commandProcessor = processor.NewSetTeamCommandProcessor( + resolver.socketModeClient, + resolver.slackBotClient, + ) + + case strings.HasPrefix(params, internal.OpenStatusModalParam): + commandProcessor = processor.NewOpenSetStatusViewModalCommandProcessor( + resolver.socketModeClient, + resolver.slackBotClient, + ) + + case strings.HasPrefix(params, internal.SetStatusParam): + commandProcessor = processor.NewSetStatusCommandProcessor( + resolver.socketModeClient, + resolver.slackBotClient, + ) + + case strings.HasPrefix(params, internal.OpenDescriptionModalParam): + commandProcessor = processor.NewOpenSetDescriptionViewModalCommandProcessor( + resolver.socketModeClient, + resolver.slackBotClient, + ) + + case strings.HasPrefix(params, internal.SetDescriptionParam): + commandProcessor = processor.NewSetDescriptionCommandProcessor( + resolver.socketModeClient, + resolver.slackBotClient, + ) + + case strings.HasPrefix(params, internal.ResolveIncidentParam): + commandProcessor = processor.NewResolveIncidentCommandProcessor( + resolver.socketModeClient, + resolver.slackBotClient, + resolver.rcaService, + ) + + case strings.HasPrefix(params, internal.OpenRCAModalParam): + commandProcessor = processor.NewOpenFillRCAViewModalCommandProcessor( + resolver.socketModeClient, + resolver.slackBotClient, + ) + + case strings.HasPrefix(params, internal.HelpParam): + commandProcessor = processor.NewHelpCommandProcessor(resolver.socketModeClient) + default: + message := fmt.Sprintf("`%s %s` is not a valid command", cmd.Command, cmd.Text) + err := appcontext.GetSlackService().PostEphemeralByChannelID(message, cmd.UserID, false, cmd.ChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to post ephemeral for invalid slash command param. %+v", logTag, err)) + } + commandProcessor = processor.NewHelpCommandProcessor(resolver.socketModeClient) + var payload interface{} + resolver.socketModeClient.Ack(*evt.Request, payload) + } + } + + return commandProcessor + +} diff --git a/internal/resolver/slash_command_resolver.go b/internal/resolver/slash_command_resolver.go deleted file mode 100644 index 5e31746..0000000 --- a/internal/resolver/slash_command_resolver.go +++ /dev/null @@ -1,38 +0,0 @@ -package resolver - -import ( - "github.com/slack-go/slack" - "github.com/slack-go/slack/socketmode" - "go.uber.org/zap" - "houston/logger" - - "houston/internal/processor" -) - -type SlashCommandResolver struct { - diagnosticCommandProcessor *processor.DiagnosticCommandProcessor - slashCommandProcessor *processor.SlashCommandProcessor -} - -func NewSlashCommandResolver( - diagnosticCommandProcessor *processor.DiagnosticCommandProcessor, - slashCommandProcessor *processor.SlashCommandProcessor, -) *SlashCommandResolver { - return &SlashCommandResolver{ - diagnosticCommandProcessor: diagnosticCommandProcessor, - slashCommandProcessor: slashCommandProcessor, - } -} - -func (scr *SlashCommandResolver) Resolve(evt *socketmode.Event) { - cmd, _ := evt.Data.(slack.SlashCommand) - switch cmd.Command { - default: - if scr.diagnosticCommandProcessor.ShouldProcessCommand(cmd.Text) { - scr.diagnosticCommandProcessor.ProcessSlashCommand(evt) - return - } - logger.Info("houston processing slash command", zap.String("command", cmd.Text), zap.String("channel_name", cmd.ChannelName), zap.String("user_name", cmd.UserName)) - scr.slashCommandProcessor.ProcessSlashCommand(evt) - } -} diff --git a/model/severity/severity.go b/model/severity/severity.go index 452df31..e75728b 100644 --- a/model/severity/severity.go +++ b/model/severity/severity.go @@ -63,7 +63,7 @@ func (s *Repository) FindSeverityById(severityId uint) (*SeverityEntity, error) func (s *Repository) FindSeverityByName(severityName string) (*SeverityEntity, error) { var severityEntity SeverityEntity - result := s.gormClient.Find(&severityEntity, "name = ?", severityName) + result := s.gormClient.Find(&severityEntity, "LOWER(name) = LOWER(?)", severityName) if result.Error != nil { return nil, result.Error } diff --git a/model/team/team.go b/model/team/team.go index e0ca131..a81456e 100644 --- a/model/team/team.go +++ b/model/team/team.go @@ -60,8 +60,7 @@ func (r *Repository) FindTeamById(teamId uint) (*TeamEntity, error) { func (r *Repository) FindTeamByTeamName(teamName string) (*TeamEntity, error) { var teamEntity TeamEntity - - result := r.gormClient.Find(&teamEntity, "name = ?", teamName) + result := r.gormClient.Find(&teamEntity, "LOWER(name) = LOWER(?)", teamName) if result.Error != nil { return nil, result.Error } else if result.RowsAffected == 0 { diff --git a/service/incident/incident_service_v2.go b/service/incident/incident_service_v2.go index c69b5f1..37ffc9a 100644 --- a/service/incident/incident_service_v2.go +++ b/service/incident/incident_service_v2.go @@ -151,10 +151,21 @@ func (i *IncidentServiceV2) CreateIncident( } func (i *IncidentServiceV2) GetIncidentById(incidentId uint) (*incident.IncidentEntity, error) { - logger.Info(fmt.Sprintf("received request to get incident by id for id: %d", incidentId)) + logger.Info(fmt.Sprintf("%s received request to get incident by id for id: %d", logTag, incidentId)) incidentEntity, err := i.incidentRepository.FindIncidentById(incidentId) if err != nil { - errorMessage := fmt.Sprintf("failed to fetch indient by id: %d", incidentId) + errorMessage := fmt.Sprintf("%s failed to fetch indient by id: %d", logTag, incidentId) + logger.Error(errorMessage, zap.Error(err)) + return nil, err + } + return incidentEntity, nil +} + +func (i *IncidentServiceV2) GetIncidentByChannelID(channelID string) (*incident.IncidentEntity, error) { + logger.Info(fmt.Sprintf("%s received request to get incident by channel ID for id: %s", logTag, channelID)) + incidentEntity, err := i.incidentRepository.FindIncidentByChannelId(channelID) + if err != nil { + errorMessage := fmt.Sprintf("%s failed to fetch indient by channel ID: %s", logTag, channelID) logger.Error(errorMessage, zap.Error(err)) return nil, err } @@ -215,6 +226,17 @@ func (i *IncidentServiceV2) GetIncidentRoleByIncidentIdAndRole( return i.incidentRepository.GetIncidentRoleByIncidentIdAndRole(incidentId, role) } +func (i *IncidentServiceV2) IsHoustonChannel(channelID string) (bool, error) { + incidentEntity, err := i.GetIncidentByChannelID(channelID) + if err != nil { + return false, err + } + if incidentEntity == nil { + return false, nil + } + return true, nil +} + const ( LinkJira string = "link" UnLinkJira = "unLink" diff --git a/service/incident/incident_service_v2_interface.go b/service/incident/incident_service_v2_interface.go index a7b8854..971e869 100644 --- a/service/incident/incident_service_v2_interface.go +++ b/service/incident/incident_service_v2_interface.go @@ -13,4 +13,5 @@ type IIncidentService interface { UnLinkJiraFromIncident(incidentId uint, unLinkedBy, jiraLink string) error GetAllOpenIncidents() ([]incident.IncidentEntity, int, error) GetIncidentRoleByIncidentIdAndRole(incidentId uint, role string) (*incident.IncidentRoleEntity, error) + IsHoustonChannel(channelID string) (bool, error) }