From 4f74943cf9746905e2751ca96e8e4d381bf09f24 Mon Sep 17 00:00:00 2001 From: Ravi Chandora Date: Mon, 10 Apr 2023 17:30:28 +0530 Subject: [PATCH] TP-0000 | Code Refactor (#10) --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 23 + .../request/severity.go | 2 +- {model => api}/request/team.go | 2 +- cmd/app/handler/severity_handler.go | 13 +- cmd/app/handler/slack_handler.go | 123 +-- cmd/app/handler/team_handler.go | 13 +- cmd/app/server.go | 10 +- cmd/app/slack.go | 17 +- cmd/main.go | 7 +- common/util/common_util.go | 14 + common/util/constant.go | 33 + config/application.properties | 7 +- config/config.go | 2 +- entity/alerts.go | 16 - entity/audit.go | 16 - entity/contributing_factor.go | 13 - entity/customer_tags.go | 13 - entity/incident.go | 54 -- entity/incident_roles.go | 24 - entity/incident_severity_team_join.go | 12 - ...cident_tags_contributing_factor_mapping.go | 14 - entity/incidents_tags_customer_mapping.go | 14 - .../incidents_tags_data_platform_mapping.go | 14 - entity/incidents_tags_mapping.go | 14 - entity/messages.go | 21 - entity/severity.go | 15 - entity/tags.go | 13 - entity/team_tags_mapping.go | 14 - entity/teams.go | 20 - entity/teams_severity_users_mapping.go | 26 - entity/users.go | 11 - go.mod | 33 +- go.sum | 709 ------------------ golang-1.png | Bin 140750 -> 0 bytes internal/cron/cron.go | 171 ++--- .../action/incident_assign_action.go | 65 +- .../incident_channel_message_update_action.go | 82 ++ .../action/incident_resolve_action.go | 43 +- .../action/incident_show_tags_action.go | 87 +++ .../incident_update_description_action.go | 46 +- .../action/incident_update_severity_action.go | 81 +- .../action/incident_update_status_action.go | 72 +- .../action/incident_update_tags_action.go | 191 +++++ .../action/incident_update_title_action.go | 50 +- .../action/incident_update_type_action.go | 147 ++++ .../processor/action/member_join_action.go | 85 +++ .../processor/action/show_incidents_action.go | 46 ++ .../processor/action/slash_command_action.go | 49 ++ .../action/start_incident_block_action.go | 53 ++ .../start_incident_modal_submission_action.go | 208 +++++ .../action/view/create_incident_modal.go | 91 +++ .../processor/action/view}/incident_assign.go | 39 +- .../action/view}/incident_description.go | 20 +- .../processor/action/view/incident_section.go | 37 +- .../action/view}/incident_severity.go | 19 +- .../view/incident_status_update_modal.go | 25 +- .../action/view/incident_summary_section.go | 40 + .../processor/action/view}/incident_title.go | 20 +- .../action/view/incident_update_tags.go | 107 +++ .../action/view}/incident_update_type.go | 19 +- .../view/show_incidents_button_section.go | 37 + .../event_type_interactive_processor.go | 236 ++++++ .../processor/events_api_event_processor.go | 43 ++ internal/processor/slash_command_processor.go | 33 + model/.DS_Store | Bin 6148 -> 0 bytes model/create_incident.go | 30 - model/request/create_message.go | 7 - model/request/incident_role.go | 10 - model/request/severity.go | 17 - pkg/postgres/config.go | 17 + pkg/postgres/query/auto_increment_id.go | 18 - pkg/postgres/query/contributing_factor.go | 71 -- pkg/postgres/query/customer_tags.go | 63 -- pkg/postgres/query/incident.go | 143 ---- pkg/postgres/query/incident_role.go | 42 -- pkg/postgres/query/incident_status.go | 45 -- .../incidents_tags_data_platform_mapping.go | 52 -- pkg/postgres/query/message_color_code.go | 14 - pkg/postgres/query/messages.go | 38 - pkg/postgres/query/severity.go | 72 -- pkg/postgres/query/tags.go | 121 --- pkg/postgres/query/teams.go | 66 -- pkg/postgres/query/users.go | 35 - pkg/postgres/service/incident/entity.go | 102 +++ pkg/postgres/service/incident/incident.go | 294 ++++++++ pkg/postgres/service/incident/model.go | 52 ++ pkg/postgres/service/role/entity.go | 12 + pkg/postgres/service/role/role.go | 34 + pkg/postgres/service/severity/entity.go | 18 + pkg/postgres/service/severity/severity.go | 62 ++ pkg/postgres/service/tag/entity.go | 34 + pkg/postgres/service/tag/model.go | 11 + pkg/postgres/service/tag/tag.go | 71 ++ pkg/postgres/service/team/entity.go | 28 + pkg/postgres/service/team/team.go | 43 ++ pkg/postgres/service/user/model.go | 14 + pkg/postgres/service/user/user.go | 1 + pkg/slack/common/channel.go | 49 -- pkg/slack/config.go | 1 - pkg/slack/houston/blazeless_main_command.go | 38 - .../houston/command/incident_show_tags.go | 108 --- .../houston/command/incident_update_tags.go | 315 -------- .../houston/command/incident_update_type.go | 123 --- .../houston/command/member_join_event.go | 56 -- pkg/slack/houston/command/show_incidents.go | 49 -- pkg/slack/houston/command/start_incident.go | 38 - .../start_incident_modal_submission.go | 244 ------ pkg/slack/houston/design/blazeless_modal.go | 92 --- ...blazeless_show_incidents_button_section.go | 29 - .../design/blazeless_summary_section.go | 80 -- .../houston/design/incident_update_tags.go | 105 --- pkg/slack/houston/slash_command_processor.go | 201 ----- pkg/{slack => slackbot}/.DS_Store | Bin pkg/slackbot/channel.go | 48 ++ pkg/slackbot/config.go | 18 + schema.sql | 241 +++--- 117 files changed, 3023 insertions(+), 4023 deletions(-) delete mode 100644 .DS_Store create mode 100644 .gitignore rename model/request/incident_status.go => api/request/severity.go (74%) rename {model => api}/request/team.go (62%) create mode 100644 common/util/common_util.go create mode 100644 common/util/constant.go delete mode 100644 entity/alerts.go delete mode 100644 entity/audit.go delete mode 100644 entity/contributing_factor.go delete mode 100644 entity/customer_tags.go delete mode 100644 entity/incident.go delete mode 100644 entity/incident_roles.go delete mode 100644 entity/incident_severity_team_join.go delete mode 100644 entity/incident_tags_contributing_factor_mapping.go delete mode 100644 entity/incidents_tags_customer_mapping.go delete mode 100644 entity/incidents_tags_data_platform_mapping.go delete mode 100644 entity/incidents_tags_mapping.go delete mode 100644 entity/messages.go delete mode 100644 entity/severity.go delete mode 100644 entity/tags.go delete mode 100644 entity/team_tags_mapping.go delete mode 100644 entity/teams.go delete mode 100644 entity/teams_severity_users_mapping.go delete mode 100644 entity/users.go delete mode 100644 go.sum delete mode 100644 golang-1.png rename pkg/slack/houston/command/incident_assign.go => internal/processor/action/incident_assign_action.go (52%) create mode 100644 internal/processor/action/incident_channel_message_update_action.go rename pkg/slack/houston/command/incident_resolve.go => internal/processor/action/incident_resolve_action.go (51%) create mode 100644 internal/processor/action/incident_show_tags_action.go rename pkg/slack/houston/command/incident_update_description.go => internal/processor/action/incident_update_description_action.go (63%) rename pkg/slack/houston/command/incident_update_severity.go => internal/processor/action/incident_update_severity_action.go (55%) rename pkg/slack/houston/command/incident_update_status.go => internal/processor/action/incident_update_status_action.go (57%) create mode 100644 internal/processor/action/incident_update_tags_action.go rename pkg/slack/houston/command/incident_update_title.go => internal/processor/action/incident_update_title_action.go (64%) create mode 100644 internal/processor/action/incident_update_type_action.go create mode 100644 internal/processor/action/member_join_action.go create mode 100644 internal/processor/action/show_incidents_action.go create mode 100644 internal/processor/action/slash_command_action.go create mode 100644 internal/processor/action/start_incident_block_action.go create mode 100644 internal/processor/action/start_incident_modal_submission_action.go create mode 100644 internal/processor/action/view/create_incident_modal.go rename {pkg/slack/houston/design => internal/processor/action/view}/incident_assign.go (61%) rename {pkg/slack/houston/design => internal/processor/action/view}/incident_description.go (55%) rename pkg/slack/houston/design/blazeless_section.go => internal/processor/action/view/incident_section.go (91%) rename {pkg/slack/houston/design => internal/processor/action/view}/incident_severity.go (70%) rename pkg/slack/houston/design/blazeless_incident_status_update_modal.go => internal/processor/action/view/incident_status_update_modal.go (63%) create mode 100644 internal/processor/action/view/incident_summary_section.go rename {pkg/slack/houston/design => internal/processor/action/view}/incident_title.go (54%) create mode 100644 internal/processor/action/view/incident_update_tags.go rename {pkg/slack/houston/design => internal/processor/action/view}/incident_update_type.go (71%) create mode 100644 internal/processor/action/view/show_incidents_button_section.go create mode 100644 internal/processor/event_type_interactive_processor.go create mode 100644 internal/processor/events_api_event_processor.go create mode 100644 internal/processor/slash_command_processor.go delete mode 100644 model/.DS_Store delete mode 100644 model/create_incident.go delete mode 100644 model/request/create_message.go delete mode 100644 model/request/incident_role.go delete mode 100644 model/request/severity.go delete mode 100644 pkg/postgres/query/auto_increment_id.go delete mode 100644 pkg/postgres/query/contributing_factor.go delete mode 100644 pkg/postgres/query/customer_tags.go delete mode 100644 pkg/postgres/query/incident.go delete mode 100644 pkg/postgres/query/incident_role.go delete mode 100644 pkg/postgres/query/incident_status.go delete mode 100644 pkg/postgres/query/incidents_tags_data_platform_mapping.go delete mode 100644 pkg/postgres/query/message_color_code.go delete mode 100644 pkg/postgres/query/messages.go delete mode 100644 pkg/postgres/query/severity.go delete mode 100644 pkg/postgres/query/tags.go delete mode 100644 pkg/postgres/query/teams.go delete mode 100644 pkg/postgres/query/users.go create mode 100644 pkg/postgres/service/incident/entity.go create mode 100644 pkg/postgres/service/incident/incident.go create mode 100644 pkg/postgres/service/incident/model.go create mode 100644 pkg/postgres/service/role/entity.go create mode 100644 pkg/postgres/service/role/role.go create mode 100644 pkg/postgres/service/severity/entity.go create mode 100644 pkg/postgres/service/severity/severity.go create mode 100644 pkg/postgres/service/tag/entity.go create mode 100644 pkg/postgres/service/tag/model.go create mode 100644 pkg/postgres/service/tag/tag.go create mode 100644 pkg/postgres/service/team/entity.go create mode 100644 pkg/postgres/service/team/team.go create mode 100644 pkg/postgres/service/user/model.go create mode 100644 pkg/postgres/service/user/user.go delete mode 100644 pkg/slack/common/channel.go delete mode 100644 pkg/slack/config.go delete mode 100644 pkg/slack/houston/blazeless_main_command.go delete mode 100644 pkg/slack/houston/command/incident_show_tags.go delete mode 100644 pkg/slack/houston/command/incident_update_tags.go delete mode 100644 pkg/slack/houston/command/incident_update_type.go delete mode 100644 pkg/slack/houston/command/member_join_event.go delete mode 100644 pkg/slack/houston/command/show_incidents.go delete mode 100644 pkg/slack/houston/command/start_incident.go delete mode 100644 pkg/slack/houston/command/start_incident_modal_submission.go delete mode 100644 pkg/slack/houston/design/blazeless_modal.go delete mode 100644 pkg/slack/houston/design/blazeless_show_incidents_button_section.go delete mode 100644 pkg/slack/houston/design/blazeless_summary_section.go delete mode 100644 pkg/slack/houston/design/incident_update_tags.go delete mode 100644 pkg/slack/houston/slash_command_processor.go rename pkg/{slack => slackbot}/.DS_Store (100%) create mode 100644 pkg/slackbot/channel.go create mode 100644 pkg/slackbot/config.go diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 979e154929e8fdb01a6be4a70ca52c943f4634b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKy-ve05I)l%Rb}bGfRKrWk6!c1MmQhEQpym z=)lB+SU~VHaOX3`ZdyiEp*zX`9N(Sq%NHe%0YEh`!a6_=04mrBi&gB#82PE?Y{tyY zAu3v<-s&cCKNlwxqJStcoeI#qTYxUa5Jo-w{oP;AJaG~Poha$xOr>-+vktG1o3=On z#oIrzR~=#+l(Aj{=Wq=Fe%36vC2G-ki`I54B*n` zQ9aZ=EWgcTrLpt8@uoE>gA ziSZF`{WjGvKp)OVpTzrC=CSMEy*%9Be?NbnXV=VooqB3W^~9z_0o=36Dzl8zivps6 zDDa~IeIGn*gucO)QGYtH(?O@$G(hwXri?g)CO-mN2I)kBKULrh Dmd>1< diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f784377 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +/.idea +.idea +/out + +go.sum + +.DS_STORE \ No newline at end of file diff --git a/model/request/incident_status.go b/api/request/severity.go similarity index 74% rename from model/request/incident_status.go rename to api/request/severity.go index 24341b4..9cc7c4a 100644 --- a/model/request/incident_status.go +++ b/api/request/severity.go @@ -1,6 +1,6 @@ package request -type AddIncidentStatusRequest struct { +type AddSeverityRequest struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` } diff --git a/model/request/team.go b/api/request/team.go similarity index 62% rename from model/request/team.go rename to api/request/team.go index 3445f58..8c1659b 100644 --- a/model/request/team.go +++ b/api/request/team.go @@ -2,5 +2,5 @@ package request type AddTeamRequest struct { Name string `json:"name,omitempty"` - OncallHandle string `json:"oncall_handle,omitempty"` + OnCallHandle string `json:"on_call_handle,omitempty"` } diff --git a/cmd/app/handler/severity_handler.go b/cmd/app/handler/severity_handler.go index e9b76f5..df0d0ae 100644 --- a/cmd/app/handler/severity_handler.go +++ b/cmd/app/handler/severity_handler.go @@ -1,8 +1,7 @@ package handler import ( - "houston/model/request" - "houston/pkg/postgres/query" + "houston/api/request" "net/http" "github.com/gin-gonic/gin" @@ -33,11 +32,11 @@ func (sh *severityHandler) AddSeverity(c *gin.Context) { sh.logger.Info("add severity request received", zap.String("severity_name", addSeverityRequest.Name)) - err := query.AddSeverity(sh.db, sh.logger, addSeverityRequest) - if err != nil { - c.JSON(http.StatusInternalServerError, err) - return - } + //err := query.AddSeverity(sh.db, sh.logger, addSeverityRequest) + //if err != nil { + // c.JSON(http.StatusInternalServerError, err) + // return + //} c.JSON(http.StatusOK, nil) diff --git a/cmd/app/handler/slack_handler.go b/cmd/app/handler/slack_handler.go index bea47e0..7ebb571 100644 --- a/cmd/app/handler/slack_handler.go +++ b/cmd/app/handler/slack_handler.go @@ -1,32 +1,41 @@ package handler import ( - "houston/pkg/slack/houston" - "github.com/slack-go/slack" "github.com/slack-go/slack/slackevents" "github.com/slack-go/slack/socketmode" "go.uber.org/zap" "gorm.io/gorm" + "houston/internal/processor" + "houston/pkg/postgres/service/incident" + "houston/pkg/postgres/service/severity" + "houston/pkg/postgres/service/tag" + "houston/pkg/postgres/service/team" + "houston/pkg/slackbot" ) type slackHandler struct { - logger *zap.Logger - socketModeClient *socketmode.Client - slackClient *slack.Client - db *gorm.DB - houstonCommandHandler *houston.HoustonCommandHandler + logger *zap.Logger + socketModeClient *socketmode.Client + slashCommandProcessor *processor.SlashCommandProcessor + memberJoinCallbackProcessor *processor.MemberJoinedCallbackEventProcessor + blockActionProcessor *processor.BlockActionProcessor + viewSubmissionProcessor *processor.ViewSubmissionProcessor } -func NewSlackHandler(logger *zap.Logger, socketModeClient *socketmode.Client, - db *gorm.DB, - slackClient *slack.Client) *slackHandler { +func NewSlackHandler(logger *zap.Logger, gormClient *gorm.DB, socketModeClient *socketmode.Client) *slackHandler { + severityService := severity.NewSeverityService(logger, gormClient) + incidentService := incident.NewIncidentService(logger, gormClient, severityService) + tagService := tag.NewTagService(logger, gormClient) + teamService := team.NewTeamService(logger, gormClient) + slackbotClient := slackbot.NewSlackClient(logger, socketModeClient) return &slackHandler{ - logger: logger, - socketModeClient: socketModeClient, - db: db, - slackClient: slackClient, - houstonCommandHandler: houston.NewHoustonCommandHandler(socketModeClient, logger, db), + logger: logger, + socketModeClient: socketModeClient, + slashCommandProcessor: processor.NewSlashCommandProcessor(logger, socketModeClient, incidentService), + memberJoinCallbackProcessor: processor.NewMemberJoinedCallbackEventProcessor(logger, socketModeClient, incidentService, teamService, severityService), + blockActionProcessor: processor.NewBlockActionProcessor(logger, socketModeClient, incidentService, teamService, severityService, tagService, slackbotClient), + viewSubmissionProcessor: processor.NewViewSubmissionProcessor(logger, socketModeClient, incidentService, teamService, severityService, tagService, slackbotClient), } } @@ -35,58 +44,64 @@ func (sh *slackHandler) HoustonConnect() { for evt := range sh.socketModeClient.Events { switch evt.Type { case socketmode.EventTypeConnecting: - sh.logger.Info("houston connecting to slack with socket mode ...") + { + sh.logger.Info("houston connecting to slackbot with socket mode") + } case socketmode.EventTypeConnectionError: - sh.logger.Error("Blazelss connection failed. retrying later ...") + { + sh.logger.Error("houston connection failed.") + } case socketmode.EventTypeConnected: - sh.logger.Info("houston connected to slack with socket mode.") + { + sh.logger.Info("houston connected to slackbot with socket mode") + } case socketmode.EventTypeEventsAPI: - ev, _ := evt.Data.(slackevents.EventsAPIEvent) - sh.logger.Info("event api", zap.Any("ev", ev)) - switch ev.Type { - case slackevents.CallbackEvent: - iev := ev.InnerEvent - switch ev := iev.Data.(type) { - case *slackevents.AppMentionEvent: - case *slackevents.MemberJoinedChannelEvent: - sh.houstonCommandHandler.ProcessMemberJoinEvent(ev, evt.Request) + { + ev, _ := evt.Data.(slackevents.EventsAPIEvent) + sh.logger.Info("event api", zap.Any("ev", ev)) + switch ev.Type { + case slackevents.CallbackEvent: + iev := ev.InnerEvent + switch ev := iev.Data.(type) { + case *slackevents.MemberJoinedChannelEvent: + sh.memberJoinCallbackProcessor.ProcessCommand(ev, evt.Request) + } } - case slackevents.URLVerification: - case string(slackevents.MemberJoinedChannel): } case socketmode.EventTypeInteractive: - callback, _ := evt.Data.(slack.InteractionCallback) + { + callback, _ := evt.Data.(slack.InteractionCallback) - switch callback.Type { - case slack.InteractionTypeBlockActions: - sh.logger.Info("received interaction type block action", - zap.String("action_id", callback.ActionID), zap.String("block_id", callback.BlockID)) - sh.houstonCommandHandler.ProcessButtonHandler(callback, evt.Request) - case slack.InteractionTypeShortcut: - case slack.InteractionTypeViewSubmission: - sh.logger.Info("received interaction type view submission", - zap.String("action_id", callback.ActionID), zap.String("block_id", callback.BlockID)) - - sh.logger.Info("payload data", zap.Any("callback", callback), zap.Any("request", evt.Request)) - sh.houstonCommandHandler.ProcessModalCallbackEvent(callback, evt.Request) - case slack.InteractionTypeDialogSubmission: - default: + switch callback.Type { + case slack.InteractionTypeBlockActions: + { + sh.logger.Info("received interaction type block action", + zap.String("action_id", callback.ActionID), zap.String("block_id", callback.BlockID)) + sh.blockActionProcessor.ProcessCommand(callback, evt.Request) + } + case slack.InteractionTypeViewSubmission: + { + sh.logger.Info("received interaction type view submission", + zap.String("action_id", callback.ActionID), zap.String("block_id", callback.BlockID)) + sh.logger.Info("payload data", zap.Any("callback", callback), zap.Any("request", evt.Request)) + sh.viewSubmissionProcessor.ProcessCommand(callback, evt.Request) + } + } } - // command.ProcessStartIncidentCommand(client, evt.Request, callback.TriggerID) case socketmode.EventTypeSlashCommand: - cmd, _ := evt.Data.(slack.SlashCommand) + { + cmd, _ := evt.Data.(slack.SlashCommand) - sh.logger.Info("houston processing slash command", - zap.String("command", cmd.Text), zap.String("channel_name", cmd.ChannelName), zap.String("user_name", cmd.UserName)) + sh.logger.Info("houston processing slash command", + zap.String("command", cmd.Text), zap.String("channel_name", cmd.ChannelName), zap.String("user_name", cmd.UserName)) - sh.houstonCommandHandler.ProcessSlashCommand(evt) - - // command.ProcessMainCommand(client, evt.Request) - - // client.Ack(*evt.Request, payload) + sh.slashCommandProcessor.ProcessSlashCommand(evt) + } default: - sh.logger.Error("houston unexpected event type received", zap.Any("event_type", evt.Type)) + { + sh.logger.Error("houston unexpected event type received", zap.Any("event_type", evt.Type)) + } } } }() diff --git a/cmd/app/handler/team_handler.go b/cmd/app/handler/team_handler.go index 0480925..8c46f6e 100644 --- a/cmd/app/handler/team_handler.go +++ b/cmd/app/handler/team_handler.go @@ -1,8 +1,7 @@ package handler import ( - "houston/model/request" - "houston/pkg/postgres/query" + "houston/api/request" "net/http" "github.com/gin-gonic/gin" @@ -32,11 +31,11 @@ func (th *teamHandler) AddTeam(c *gin.Context) { } th.logger.Info("add team request received", zap.String("team_name", addTeamRequest.Name)) - err := query.AddTeam(th.db, addTeamRequest) - if err != nil { - c.JSON(http.StatusInternalServerError, err) - return - } + //err := query.AddTeam(th.db, addTeamRequest) + //if err != nil { + // c.JSON(http.StatusInternalServerError, err) + // return + //} c.JSON(http.StatusOK, nil) } diff --git a/cmd/app/server.go b/cmd/app/server.go index addb381..6586f3b 100644 --- a/cmd/app/server.go +++ b/cmd/app/server.go @@ -2,13 +2,11 @@ package app import ( "fmt" - "houston/cmd/app/handler" - "houston/internal/cron" - "github.com/gin-gonic/gin" "github.com/spf13/viper" "go.uber.org/zap" "gorm.io/gorm" + "houston/cmd/app/handler" ) type Server struct { @@ -29,14 +27,14 @@ func (s *Server) Handler() { s.teamHandler() s.severityHandler() - //this should always be at the end since it opens websocket to connect to slack + //this should always be at the end since it opens websocket to connect to slackbot s.houstonHandler() } func (s *Server) houstonHandler() { houstonClient := NewHoustonClient(s.logger) - houstonHandler := handler.NewSlackHandler(s.logger, houstonClient.socketmodeClient, s.db, houstonClient.slackClient) - cron.RunJob(houstonClient.slackClient, s.db, s.logger) + houstonHandler := handler.NewSlackHandler(s.logger, s.db, houstonClient.socketModeClient) + //cron.RunJob(houstonClient.slackClient, s.db, s.logger) houstonHandler.HoustonConnect() } diff --git a/cmd/app/slack.go b/cmd/app/slack.go index ed32a48..a1187f6 100644 --- a/cmd/app/slack.go +++ b/cmd/app/slack.go @@ -1,6 +1,7 @@ package app import ( + "github.com/spf13/viper" "os" "strings" @@ -10,23 +11,21 @@ import ( ) type HoustonSlack struct { - socketmodeClient *socketmode.Client - slackClient *slack.Client + socketModeClient *socketmode.Client logger *zap.Logger } func NewHoustonClient(logger *zap.Logger) *HoustonSlack { - socketmodeClient, slackClient := slackConnect(logger) + socketModeClient := slackConnect(logger) return &HoustonSlack{ - socketmodeClient: socketmodeClient, - slackClient: slackClient, + socketModeClient: socketModeClient, logger: logger, } } -func slackConnect(logger *zap.Logger) (*socketmode.Client, *slack.Client) { - appToken := os.Getenv("HOUSTON_SLACK_APP_TOKEN") +func slackConnect(logger *zap.Logger) *socketmode.Client { + appToken := viper.GetString("HOUSTON_SLACK_APP_TOKEN") if appToken == "" { logger.Error("HOUSTON_SLACK_APP_TOKEN must be set.") os.Exit(1) @@ -37,7 +36,7 @@ func slackConnect(logger *zap.Logger) (*socketmode.Client, *slack.Client) { os.Exit(1) } - botToken := os.Getenv("HOUSTON_SLACK_BOT_TOKEN") + botToken := viper.GetString("HOUSTON_SLACK_BOT_TOKEN") if botToken == "" { logger.Error("HOUSTON_SLACK_BOT_TOKEN must be set.") os.Exit(1) @@ -59,5 +58,5 @@ func slackConnect(logger *zap.Logger) (*socketmode.Client, *slack.Client) { socketmode.OptionDebug(false), ) - return client, api + return client } diff --git a/cmd/main.go b/cmd/main.go index 0b383ed..5ea016a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,6 +1,7 @@ package main import ( + "github.com/spf13/viper" "houston/cmd/app" "houston/config" "houston/pkg/postgres" @@ -21,15 +22,15 @@ func main() { command := &cobra.Command{ Use: "houston", - Short: "houston is replacement for blameless and incident management slack bot at Navi", - Long: "houston is replacement for blameless and incident management slack bot at Navi", + Short: "houston is replacement for blameless and incident management slackbot bot at Navi", + Long: "houston is replacement for blameless and incident management slackbot bot at Navi", RunE: func(cmd *cobra.Command, args []string) error { r := gin.New() r.Use(ginzap.Ginzap(logger, time.RFC3339, true)) r.Use(ginzap.RecoveryWithZap(logger, true)) - db := postgres.PQConnection(logger) + db := postgres.NewGormClient(viper.GetString("POSTGRES_DSN"), logger) sv := app.NewServer(r, logger, db) sv.Handler() diff --git a/common/util/common_util.go b/common/util/common_util.go new file mode 100644 index 0000000..1559458 --- /dev/null +++ b/common/util/common_util.go @@ -0,0 +1,14 @@ +package util + +func GetColorBySeverity(severityId uint) string { + switch severityId { + case 1: + return "#fc3838" + case 2: + return "#fc9338" + case 3: + return "#ebfa1b" + default: + return "#7288db" + } +} diff --git a/common/util/constant.go b/common/util/constant.go new file mode 100644 index 0000000..a2957d9 --- /dev/null +++ b/common/util/constant.go @@ -0,0 +1,33 @@ +package util + +type BlockActionType string + +const ( + StartIncident BlockActionType = "start_incident" + ShowIncidents = "show_incidents" + Incident = "incident" + Tags = "tags" + AssignIncidentRole = "assign_incident_role" + ResolveIncident = "resolve_incident" + SetIncidentStatus = "set_incident_status" + SetIncidentType = "set_incident_type" + SetIncidentSeverity = "set_incident_severity" + SetIncidentTitle = "set_incident_title" + SetIncidentDescription = "set_incident_description" + AddTags = "add_tags" + ShowTags = "show_tags" + RemoveTag = "remove_tags" +) + +type ViewSubmissionType string + +const ( + StartIncidentSubmit ViewSubmissionType = "start_incident_submit" + AssignIncidentRoleSubmit = "assign_incident_role_submit" + SetIncidentStatusSubmit = "set_incident_status_submit" + SetIncidentTitleSubmit = "set_incident_title_submit" + SetIncidentDescriptionSubmit = "set_incident_description_submit" + SetIncidentSeveritySubmit = "set_incident_severity_submit" + SetIncidentTypeSubmit = "set_incident_type_submit" + UpdateTagSubmit = "updateTagSubmit" +) diff --git a/config/application.properties b/config/application.properties index 9e97d01..52d97d1 100644 --- a/config/application.properties +++ b/config/application.properties @@ -1,6 +1,7 @@ -HOUSTON_SLACK_APP_TOKEN=xapp-1-A0444DP8XU5-4136817231606-8e441610b1696fd4f5fa81e9507a3d6f625504ac07588c63543972ff3b147321 -HOUSTON_SLACK_BOT_TOKEN=j22 +HOUSTON_SLACK_APP_TOKEN=xapp-1-A04TBQ7PGSJ-4960174100544-3c648a093c830a718bd81aff36cf0f433633312e16a0a6e11408bf5063a4785d +HOUSTON_SLACK_BOT_TOKEN=token ENVIRONMENT=local SHOW_INCIDENTS_LIMIT=10 PORT=8080 -METRIC_PORT=9090 \ No newline at end of file +METRIC_PORT=9090 +POSTGRES_DSN=postgresql://postgres:admin@localhost:5432/houston diff --git a/config/config.go b/config/config.go index 2f62b63..6a593b4 100644 --- a/config/config.go +++ b/config/config.go @@ -14,7 +14,7 @@ func LoadHoustonConfig(logger *zap.Logger) { err := viper.ReadInConfig() if err != nil { - logger.Error("Error while loading blazeless configuration", zap.Error(err)) + logger.Error("Error while loading houston configuration", zap.Error(err)) os.Exit(1) } } diff --git a/entity/alerts.go b/entity/alerts.go deleted file mode 100644 index bf8a239..0000000 --- a/entity/alerts.go +++ /dev/null @@ -1,16 +0,0 @@ -package entity - -import "gorm.io/gorm" - -type AlertsEntity struct { - gorm.Model - Name string - Team string - Service string - Status bool - Version int -} - -func (AlertsEntity) TableName() string { - return "alerts" -} diff --git a/entity/audit.go b/entity/audit.go deleted file mode 100644 index 08b60d8..0000000 --- a/entity/audit.go +++ /dev/null @@ -1,16 +0,0 @@ -package entity - -import "gorm.io/gorm" - -type IncidentAuditEntity struct { - gorm.Model - IncidentId uint - Event string - UserName string - UserId string - Version int -} - -func (IncidentAuditEntity) TableName() string { - return "incident_audit" -} diff --git a/entity/contributing_factor.go b/entity/contributing_factor.go deleted file mode 100644 index 3aa7c6d..0000000 --- a/entity/contributing_factor.go +++ /dev/null @@ -1,13 +0,0 @@ -package entity - -import "gorm.io/gorm" - -type ContributingFactorEntity struct { - gorm.Model - Label string - Version int -} - -func (ContributingFactorEntity) TableName() string { - return "contributing_factor" -} diff --git a/entity/customer_tags.go b/entity/customer_tags.go deleted file mode 100644 index f82c956..0000000 --- a/entity/customer_tags.go +++ /dev/null @@ -1,13 +0,0 @@ -package entity - -import "gorm.io/gorm" - -type CustomerTagsEntity struct { - gorm.Model - Label string - Version int -} - -func (CustomerTagsEntity) TableName() string { - return "customer_tags" -} diff --git a/entity/incident.go b/entity/incident.go deleted file mode 100644 index 1a46ed5..0000000 --- a/entity/incident.go +++ /dev/null @@ -1,54 +0,0 @@ -package entity - -import ( - "time" - - "gorm.io/gorm" -) - -type IncidentStatus string - -const ( - Investigating IncidentStatus = "INVESTIGATING" - Identified = "IDENTIFIED" - Monitoring = "MONITORING" - Resolved = "RESOLVED" - Duplicated = "DUPLICATED" -) - -type IncidentEntity struct { - gorm.Model - Title string - Description string - Status IncidentStatus - SeverityId int - IncidentName string - SlackChannel string - DetectionTime *time.Time - CustomerImpactStartTime time.Time - CustomerImpactEndTime *time.Time - TeamsId int - JiraId string - ConfluenceId string - SeverityTat time.Time - RemindMeAt *time.Time - EnableReminder bool - CreatedBy string - UpdatedBy string - Version int -} - -func (IncidentEntity) TableName() string { - return "incidents" -} - -type IncidentStatusEntity struct { - gorm.Model - Name string - Description string - Version int -} - -func (IncidentStatusEntity) TableName() string { - return "incident_status" -} diff --git a/entity/incident_roles.go b/entity/incident_roles.go deleted file mode 100644 index a9d2dec..0000000 --- a/entity/incident_roles.go +++ /dev/null @@ -1,24 +0,0 @@ -package entity - -import "gorm.io/gorm" - -type IncidentRole string - -const ( - RETROSPECTIVE IncidentRole = "RETROSPECTIVE" - RESPONDER IncidentRole = "RESPONDER" - SERVICE_OWNER IncidentRole = "SERVICE_OWNER" -) - -type IncidentRoles struct { - gorm.Model - IncidentId int - Role IncidentRole - AssignedToUserSlackId string - AssignedByUserSlackId string - Version int -} - -func (IncidentRoles) TableName() string { - return "incident_roles" -} diff --git a/entity/incident_severity_team_join.go b/entity/incident_severity_team_join.go deleted file mode 100644 index a1f5753..0000000 --- a/entity/incident_severity_team_join.go +++ /dev/null @@ -1,12 +0,0 @@ -package entity - -type IncidentSeverityTeamJoinEntity struct { - IncidentId int - Title string - Status IncidentStatus - SeverityId int - SeverityName string - SlackChannel string - TeamsId int - TeamsName string -} diff --git a/entity/incident_tags_contributing_factor_mapping.go b/entity/incident_tags_contributing_factor_mapping.go deleted file mode 100644 index 3ba9b8c..0000000 --- a/entity/incident_tags_contributing_factor_mapping.go +++ /dev/null @@ -1,14 +0,0 @@ -package entity - -import "gorm.io/gorm" - -type IncidentTagsContributingFactorMapping struct { - gorm.Model - IncidentId int - ContributingFactorId int - Version int -} - -func (IncidentTagsContributingFactorMapping) TableName() string { - return "incidents_tags_contributing_factor_mapping" -} diff --git a/entity/incidents_tags_customer_mapping.go b/entity/incidents_tags_customer_mapping.go deleted file mode 100644 index 5f2d1ab..0000000 --- a/entity/incidents_tags_customer_mapping.go +++ /dev/null @@ -1,14 +0,0 @@ -package entity - -import "gorm.io/gorm" - -type IncidentTagsCustomerMapping struct { - gorm.Model - IncidentId int - CustomerTagsId int - Version int -} - -func (IncidentTagsCustomerMapping) TableName() string { - return "incidents_tags_customer_mapping" -} diff --git a/entity/incidents_tags_data_platform_mapping.go b/entity/incidents_tags_data_platform_mapping.go deleted file mode 100644 index 00fbb9e..0000000 --- a/entity/incidents_tags_data_platform_mapping.go +++ /dev/null @@ -1,14 +0,0 @@ -package entity - -import "gorm.io/gorm" - -type IncidentsTagsDataPlatformMapping struct { - gorm.Model - IncidentId int - DataPlatformTag string - Version int -} - -func (IncidentsTagsDataPlatformMapping) TableName() string { - return "incidents_tags_data_platform_mapping" -} diff --git a/entity/incidents_tags_mapping.go b/entity/incidents_tags_mapping.go deleted file mode 100644 index 20e6032..0000000 --- a/entity/incidents_tags_mapping.go +++ /dev/null @@ -1,14 +0,0 @@ -package entity - -import "gorm.io/gorm" - -type IncidentsTagsMapping struct { - gorm.Model - IncidentId int - TagId int - Version int -} - -func (IncidentsTagsMapping) TableName() string { - return "incidents_tags_mapping" -} diff --git a/entity/messages.go b/entity/messages.go deleted file mode 100644 index 78b5758..0000000 --- a/entity/messages.go +++ /dev/null @@ -1,21 +0,0 @@ -package entity - -import ( - "time" - - "gorm.io/gorm" -) - -type MessageEntity struct { - gorm.Model - SlackChannel string `gorm:"column:slack_channel"` - IncidentName string `gorm:"column:incident_name"` - MessageTimeStamp string `gorm:"column:message_timestamp"` - CreatedAt time.Time - UpdatedAt time.Time - Version int -} - -func (MessageEntity) TableName() string { - return "messages" -} diff --git a/entity/severity.go b/entity/severity.go deleted file mode 100644 index e22399c..0000000 --- a/entity/severity.go +++ /dev/null @@ -1,15 +0,0 @@ -package entity - -import "gorm.io/gorm" - -type SeverityEntity struct { - gorm.Model - Name string `gorm:"column:name"` - Description string `gorm:"column:description"` - Version int - Sla int -} - -func (SeverityEntity) TableName() string { - return "severity" -} diff --git a/entity/tags.go b/entity/tags.go deleted file mode 100644 index 1d8669e..0000000 --- a/entity/tags.go +++ /dev/null @@ -1,13 +0,0 @@ -package entity - -import "gorm.io/gorm" - -type TagsEntity struct { - gorm.Model - Label string `gorm:"column:label"` - Version int -} - -func (TagsEntity) TableName() string { - return "tags" -} diff --git a/entity/team_tags_mapping.go b/entity/team_tags_mapping.go deleted file mode 100644 index 7a27f34..0000000 --- a/entity/team_tags_mapping.go +++ /dev/null @@ -1,14 +0,0 @@ -package entity - -import "gorm.io/gorm" - -type TeamTagsMapping struct { - gorm.Model - TeamsId int - TagId int - Version int -} - -func (TeamTagsMapping) TableName() string { - return "teams_tags_mapping" -} diff --git a/entity/teams.go b/entity/teams.go deleted file mode 100644 index f3c3739..0000000 --- a/entity/teams.go +++ /dev/null @@ -1,20 +0,0 @@ -package entity - -import ( - "gorm.io/gorm" -) - -type TeamEntity struct { - gorm.Model - Name string `gorm:"column:name"` - OncallHandle string `gorm:"column:oncall_handle"` - SecondaryOncallHandle string `gorm:"column:secondary_oncall_handle"` - ManagerHandle string `gorm:"column:manager_handle"` - SecondaryManagerHandle string `gorm:"column:secondary_manager_handle"` - Active bool `gorm:"column:active"` - Version int `gorm:"column:version"` -} - -func (TeamEntity) TableName() string { - return "teams" -} diff --git a/entity/teams_severity_users_mapping.go b/entity/teams_severity_users_mapping.go deleted file mode 100644 index 530a9db..0000000 --- a/entity/teams_severity_users_mapping.go +++ /dev/null @@ -1,26 +0,0 @@ -package entity - -import ( - "gorm.io/gorm" -) - -type EntityType string - -const ( - TEAM EntityType = "TEAM" - SEVERITY EntityType = "SEVERITY" -) - -type TeamsSeverityUsersMapping struct { - gorm.Model - EntityType EntityType - EntityId int - UsersId int - DefaultAddInIncidents bool - teamRole string - Version int -} - -func (TeamsSeverityUsersMapping) TableName() string { - return "teams_severity_user_mapping" -} diff --git a/entity/users.go b/entity/users.go deleted file mode 100644 index b56a2fb..0000000 --- a/entity/users.go +++ /dev/null @@ -1,11 +0,0 @@ -package entity - -type UsersEntity struct { - Name string - SlackUserId string - Active bool -} - -func (UsersEntity) TableName() string { - return "users" -} diff --git a/go.mod b/go.mod index 337d3ed..aaf5384 100644 --- a/go.mod +++ b/go.mod @@ -1,60 +1,38 @@ module houston -go 1.20 +go 1.19 require ( - github.com/Shopify/sarama v1.38.1 github.com/gin-contrib/zap v0.1.0 github.com/gin-gonic/gin v1.9.0 - github.com/google/uuid v1.3.0 github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.7 github.com/slack-go/slack v0.12.1 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.15.0 go.uber.org/zap v1.24.0 - google.golang.org/api v0.107.0 gorm.io/driver/postgres v1.4.8 gorm.io/gorm v1.24.5 ) require ( - cloud.google.com/go/compute v1.14.0 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/bytedance/sonic v1.8.2 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/eapache/go-resiliency v1.3.0 // indirect - github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 // indirect - github.com/eapache/queue v1.1.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.11.2 // indirect github.com/goccy/go-json v0.10.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.1 // indirect - github.com/googleapis/gax-go/v2 v2.7.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.3.0 // indirect - github.com/jcmturner/aescts/v2 v2.0.0 // indirect - github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect - github.com/jcmturner/gofork v1.7.6 // indirect - github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect - github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.16.0 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -63,8 +41,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect - github.com/pierrec/lz4/v4 v4.1.17 // indirect - github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/spf13/afero v1.9.4 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect @@ -72,7 +48,6 @@ require ( github.com/subosito/gotenv v1.4.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.10 // indirect - go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel v1.13.0 // indirect go.opentelemetry.io/otel/trace v1.13.0 // indirect go.uber.org/atomic v1.10.0 // indirect @@ -80,12 +55,8 @@ require ( golang.org/x/arch v0.2.0 // indirect golang.org/x/crypto v0.6.0 // indirect golang.org/x/net v0.7.0 // indirect - golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect golang.org/x/sys v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect - google.golang.org/grpc v1.52.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum deleted file mode 100644 index 6ed13a5..0000000 --- a/go.sum +++ /dev/null @@ -1,709 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0= -cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A= -github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= -github.com/Shopify/toxiproxy/v2 v2.5.0 h1:i4LPT+qrSlKNtQf5QliVjdP08GyAH8+BUIc9gT0eahc= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.8.2 h1:Eq1oE3xWIBE3tj2ZtJFK1rDAx7+uA4bRytozVhXMHKY= -github.com/bytedance/sonic v1.8.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/eapache/go-resiliency v1.3.0 h1:RRL0nge+cWGlxXbUzJ7yMcq6w2XBEr19dCN6HECGaT0= -github.com/eapache/go-resiliency v1.3.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= -github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 h1:8yY/I9ndfrgrXUbOGObLHKBR4Fl3nZXwM2c7OYTT8hM= -github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= -github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-contrib/zap v0.1.0 h1:RMSFFJo34XZogV62OgOzvrlaMNmXrNxmJ3bFmMwl6Cc= -github.com/gin-contrib/zap v0.1.0/go.mod h1:hvnZaPs478H1PGvRP8w89ZZbyJUiyip4ddiI/53WG3o= -github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= -github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= -github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= -github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= -github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= -github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= -github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA= -github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= -github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= -github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= -github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= -github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= -github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= -github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= -github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= -github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= -github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/slack-go/slack v0.12.1 h1:X97b9g2hnITDtNsNe5GkGx6O2/Sz/uC20ejRZN6QxOw= -github.com/slack-go/slack v0.12.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= -github.com/spf13/afero v1.9.4 h1:Sd43wM1IWz/s1aVXdOBkjJvuP8UdyqioeE4AmM0QsBs= -github.com/spf13/afero v1.9.4/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= -github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -github.com/ugorji/go/codec v1.2.10 h1:eimT6Lsr+2lzmSZxPhLFoOWFmQqwk0fllJJ5hEbTXtQ= -github.com/ugorji/go/codec v1.2.10/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/otel v1.10.0/go.mod h1:NbvWjCthWHKBEUMpf0/v8ZRZlni86PpGFEMA9pnQSnQ= -go.opentelemetry.io/otel v1.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y= -go.opentelemetry.io/otel v1.13.0/go.mod h1:FH3RtdZCzRkJYFTCsAKDy9l/XYjMdNv6QrkFFB8DvVg= -go.opentelemetry.io/otel/trace v1.10.0/go.mod h1:Sij3YYczqAdz+EhmGhE6TpTxUO5/F/AzrK+kxfGqySM= -go.opentelemetry.io/otel/trace v1.13.0 h1:CBgRZ6ntv+Amuj1jDsMhZtlAPT6gbyIRdaIzFhfBSdY= -go.opentelemetry.io/otel/trace v1.13.0/go.mod h1:muCvmmO9KKpvuXSf3KKAXXB2ygNYHQ+ZfI5X08d3tds= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.2.0 h1:W1sUEHXiJTfjaFJ5SLo0N6lZn+0eO5gWD1MFeTGqQEY= -golang.org/x/arch v0.2.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.107.0 h1:I2SlFjD8ZWabaIFOfeEDg3pf0BHJDh6iYQ1ic3Yu/UU= -google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY= -google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= -google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.4.8 h1:NDWizaclb7Q2aupT0jkwK8jx1HVCNzt+PQ8v/VnxviA= -gorm.io/driver/postgres v1.4.8/go.mod h1:O9MruWGNLUBUWVYfWuBClpf3HeGjOoybY0SNmCs3wsw= -gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE= -gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/golang-1.png b/golang-1.png deleted file mode 100644 index 6ea687b1d4bc3c2fbe827d7f4fa6642e22f91d25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 140750 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≪ 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 diff --git a/internal/cron/cron.go b/internal/cron/cron.go index bd44fe1..9751265 100644 --- a/internal/cron/cron.go +++ b/internal/cron/cron.go @@ -1,87 +1,88 @@ package cron -import ( - "fmt" - "houston/pkg/postgres/query" - "houston/pkg/slack/houston" - "time" - - "github.com/robfig/cron" - "github.com/slack-go/slack" - "go.uber.org/zap" - "gorm.io/gorm" -) - -func RunJob(slackClient *slack.Client, db *gorm.DB, logger *zap.Logger) { - c := cron.New() - //RUN EVERY HOUR - c.AddFunc("0 0 * * * *", func() { - fmt.Println("Running job at", time.Now().Format(time.RFC3339)) - - severityUserMap := make(map[int][]string) - severityData, err := query.FindSeverity(db, logger) - for _, o := range severityData { - userIdList, err := query.FindDefaultUserIdToBeAddedBySeverity(db, int(o.ID)) - if err != nil { - logger.Error("FindDefaultUserIdToBeAddedBySeverity error in cron job") - return - } - severityUserMap[int(o.ID)] = userIdList - } - - //FETCH INCIDENTS WHICH ARE NOT RESOLVED AND CURRENT TIMESTAMP>=SEVERITY TAT FIELD - incidents, err := query.FindIncidentsBreachingSevTat(db) - if err != nil { - logger.Error("FindIncidentsBreachingSevTat error", - zap.Error(err)) - return - } - for i := 0; i < len(incidents); i++ { - var currentSeverityId int = incidents[i].SeverityId - var severityString string - //CHECK IF SEVERITY IS ALREADY 0 OR NOT. SEV-0 is saved as id = 1 in db - if currentSeverityId-1 > 0 { - incidents[i].SeverityId = currentSeverityId - 1 - severityEntity, err := query.FindSeverityById(db, incidents[i].SeverityId) - if err != nil { - logger.Error("failed to fetch FindSeverityByIdin cron job", - zap.Int("severityId", incidents[i].SeverityId), zap.Error(err)) - } - incidents[i].SeverityTat = time.Now().AddDate(0, 0, severityEntity.Sla) - severityString = fmt.Sprintln(severityEntity.Name + " (" + severityEntity.Description + ")") - } - - incidents[i].UpdatedAt = time.Now() - err = query.UpdateIncident(db, &incidents[i]) - if err != nil { - logger.Error("failed to update incident in cron job", - zap.String("channel", incidents[i].SlackChannel), zap.Error(err)) - } - - //DEFAULT USER ADDITION - for _, o := range severityUserMap[incidents[i].SeverityId] { - //throws error if the customer is already present in channel - _, err := slackClient.InviteUsersToConversation(incidents[i].SlackChannel, o) - if err != nil { - logger.Error("Slack Client InviteUsersToConversation error in cron job") - return - } - } - - //UPDATING MESSAGE - houston.UpdateMessage(db, &incidents[i], nil, slackClient) - - //SENDING MESSAGE IN THE CHANNEL - if currentSeverityId > 1 { - msgOption := slack.MsgOptionText(fmt.Sprintf("Issue has been escalated to "+severityString+" as there was TAT breach"), true) - _, _, errMessage := slackClient.PostMessage(incidents[i].SlackChannel, msgOption) - if errMessage != nil { - logger.Error("PostMessage failed for cronJob ", zap.Error(errMessage), zap.Int("incidentId", int(incidents[i].ID))) - return - } - } - } - - }) - c.Start() -} +// +//import ( +// "fmt" +// "houston/internal/processor" +// "houston/pkg/postgres/query" +// "time" +// +// "github.com/robfig/cron" +// "github.com/slack-go/slack" +// "go.uber.org/zap" +// "gorm.io/gorm" +//) +// +//func RunJob(slackClient *slack.Client, db *gorm.DB, logger *zap.Logger) { +// c := cron.New() +// //RUN EVERY HOUR +// c.AddFunc("0 0 * * * *", func() { +// fmt.Println("Running job at", time.Now().Format(time.RFC3339)) +// +// severityUserMap := make(map[int][]string) +// severityData, err := query.FindSeverity(db, logger) +// for _, o := range severityData { +// userIdList, err := query.FindDefaultUserIdToBeAddedBySeverity(db, int(o.ID)) +// if err != nil { +// logger.Error("FindDefaultUserIdToBeAddedBySeverity error in cron job") +// return +// } +// severityUserMap[int(o.ID)] = userIdList +// } +// +// //FETCH INCIDENTS WHICH ARE NOT RESOLVED AND CURRENT TIMESTAMP>=SEVERITY TAT FIELD +// incidents, err := query.FindIncidentsBreachingSevTat(db) +// if err != nil { +// logger.Error("FindIncidentsBreachingSevTat error", +// zap.Error(err)) +// return +// } +// for i := 0; i < len(incidents); i++ { +// var currentSeverityId int = incidents[i].SeverityId +// var severityString string +// //CHECK IF SEVERITY IS ALREADY 0 OR NOT. SEV-0 is saved as id = 1 in db +// if currentSeverityId-1 > 0 { +// incidents[i].SeverityId = currentSeverityId - 1 +// severityEntity, err := query.FindSeverityById(db, incidents[i].SeverityId) +// if err != nil { +// logger.Error("failed to fetch FindSeverityByIdin cron job", +// zap.Int("severityId", incidents[i].SeverityId), zap.Error(err)) +// } +// incidents[i].SeverityTat = time.Now().AddDate(0, 0, severityEntity.Sla) +// severityString = fmt.Sprintln(severityEntity.Name + " (" + severityEntity.Description + ")") +// } +// +// incidents[i].UpdatedAt = time.Now() +// err = query.UpdateIncident(db, &incidents[i]) +// if err != nil { +// logger.Error("failed to update incident in cron job", +// zap.String("channel", incidents[i].SlackChannel), zap.Error(err)) +// } +// +// //DEFAULT USER ADDITION +// for _, o := range severityUserMap[incidents[i].SeverityId] { +// //throws error if the customer is already present in channel +// _, err := slackClient.InviteUsersToConversation(incidents[i].SlackChannel, o) +// if err != nil { +// logger.Error("Slack Client InviteUsersToConversation error in cron job") +// return +// } +// } +// +// //UPDATING MESSAGE +// processor.UpdateMessage(db, &incidents[i], nil, slackClient) +// +// //SENDING MESSAGE IN THE CHANNEL +// if currentSeverityId > 1 { +// msgOption := slack.MsgOptionText(fmt.Sprintf("Issue has been escalated to "+severityString+" as there was TAT breach"), true) +// _, _, errMessage := slackClient.PostMessage(incidents[i].SlackChannel, msgOption) +// if errMessage != nil { +// logger.Error("PostMessage failed for cronJob ", zap.Error(errMessage), zap.Int("incidentId", int(incidents[i].ID))) +// return +// } +// } +// } +// +// }) +// c.Start() +//} diff --git a/pkg/slack/houston/command/incident_assign.go b/internal/processor/action/incident_assign_action.go similarity index 52% rename from pkg/slack/houston/command/incident_assign.go rename to internal/processor/action/incident_assign_action.go index 659c657..8d8b071 100644 --- a/pkg/slack/houston/command/incident_assign.go +++ b/internal/processor/action/incident_assign_action.go @@ -1,38 +1,35 @@ -package command +package action import ( "encoding/json" "fmt" - "houston/model/request" - "houston/pkg/postgres/query" - houston "houston/pkg/slack/houston/design" - "github.com/slack-go/slack" "github.com/slack-go/slack/socketmode" "go.uber.org/zap" - "gorm.io/gorm" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/incident" ) -type incidentAssignProcessor struct { - client *socketmode.Client - db *gorm.DB - logger *zap.Logger +type AssignIncidentAction struct { + client *socketmode.Client + logger *zap.Logger + incidentService *incident.Service } -func NewIncidentAssignProcessor(client *socketmode.Client, db *gorm.DB, logger *zap.Logger) *incidentAssignProcessor { - return &incidentAssignProcessor{ - client: client, - db: db, - logger: logger, +func NewAssignIncidentAction(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Service) *AssignIncidentAction { + return &AssignIncidentAction{ + client: client, + logger: logger, + incidentService: incidentService, } } -func (iap *incidentAssignProcessor) IncidentAssignProcess(callback slack.InteractionCallback, request *socketmode.Request) { +func (iap *AssignIncidentAction) IncidentAssignProcess(callback slack.InteractionCallback, request *socketmode.Request) { - modalRequest := houston.GenerateModalForIncidentAssign(callback.Channel) + modalRequest := view.GenerateModalForIncidentAssign(callback.Channel) _, err := iap.client.OpenView(callback.TriggerID, modalRequest) if err != nil { - iap.logger.Error("houston slack openview command failed.", + iap.logger.Error("houston slackbot open view command failed.", zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) return } @@ -41,27 +38,30 @@ func (iap *incidentAssignProcessor) IncidentAssignProcess(callback slack.Interac } -func (iap *incidentAssignProcessor) IncidentAssignModalCommandProcessing(callback slack.InteractionCallback, request *socketmode.Request) { - incidentEntity, err := query.FindIncidentByChannelId(iap.db, callback.View.PrivateMetadata) +func (iap *AssignIncidentAction) IncidentAssignModalCommandProcessing(callback slack.InteractionCallback, request *socketmode.Request) { + incidentEntity, err := iap.incidentService.FindIncidentByChannelId(callback.View.PrivateMetadata) if err != nil { iap.logger.Error("FindIncidentByChannelId error", zap.String("incident_slack_channel_id", callback.View.PrivateMetadata), zap.String("user_id", callback.User.ID), zap.Error(err)) + return } else if incidentEntity == nil { iap.logger.Error("IncidentEntity not found ", zap.String("incident_slack_channel_id", callback.View.PrivateMetadata), zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), zap.Error(err)) return } - assignIncidentRoleRequest := buildAssignIncidentRoleRequest(callback.View.State.Values) - assignIncidentRoleRequest.CreatedById = callback.User.ID - assignIncidentRoleRequest.IncidentId = int(incidentEntity.ID) + + assignIncidentRoleRequest := buildAssignIncidentRoleRequest(callback, incidentEntity) + iap.logger.Info("request", zap.Any("request", assignIncidentRoleRequest)) - err = query.UpsertIncidentRole(iap.db, assignIncidentRoleRequest) + + err = iap.incidentService.UpsertIncidentRole(assignIncidentRoleRequest) if err != nil { iap.logger.Error("UpsertIncidentRole failed", zap.Error(err)) return } + msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> is assigned to %s by <@%s>", assignIncidentRoleRequest.UserId, assignIncidentRoleRequest.Role, assignIncidentRoleRequest.CreatedById), false) _, _, errMessage := iap.client.PostMessage(callback.View.PrivateMetadata, msgOption) if errMessage != nil { @@ -74,21 +74,24 @@ func (iap *incidentAssignProcessor) IncidentAssignModalCommandProcessing(callbac } -func buildAssignIncidentRoleRequest(blockActions map[string]map[string]slack.BlockAction) *request.AddIncidentRoleRequest { - var addIncidentRoleRequest request.AddIncidentRoleRequest +func buildAssignIncidentRoleRequest(callback slack.InteractionCallback, incidentEntity *incident.IncidentEntity) *incident.AddIncidentRoleRequest { + blockActions := callback.View.State.Values + var addIncidentRoleRequest incident.AddIncidentRoleRequest var requestMap = make(map[string]string, 0) for _, actions := range blockActions { - for actionID, action := range actions { - if action.Type == "users_select" { - requestMap[actionID] = action.SelectedUser + for actionID, a := range actions { + if a.Type == "users_select" { + requestMap[actionID] = a.SelectedUser } - if action.Type == "static_select" { - requestMap[actionID] = action.SelectedOption.Value + if a.Type == "static_select" { + requestMap[actionID] = a.SelectedOption.Value } } } desRequestMap, _ := json.Marshal(requestMap) json.Unmarshal(desRequestMap, &addIncidentRoleRequest) + addIncidentRoleRequest.CreatedById = callback.User.ID + addIncidentRoleRequest.IncidentId = int(incidentEntity.ID) return &addIncidentRoleRequest } diff --git a/internal/processor/action/incident_channel_message_update_action.go b/internal/processor/action/incident_channel_message_update_action.go new file mode 100644 index 0000000..9f0a43c --- /dev/null +++ b/internal/processor/action/incident_channel_message_update_action.go @@ -0,0 +1,82 @@ +package action + +import ( + "errors" + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/common/util" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/incident" + "houston/pkg/postgres/service/severity" + "houston/pkg/postgres/service/team" +) + +type IncidentChannelMessageUpdateAction struct { + socketModeClient *socketmode.Client + logger *zap.Logger + incidentService *incident.Service + teamService *team.Service + severityService *severity.Service +} + +func NewIncidentChannelMessageUpdateAction(socketModeClient *socketmode.Client, logger *zap.Logger, + incidentService *incident.Service, teamService *team.Service, severityService *severity.Service) *IncidentChannelMessageUpdateAction { + return &IncidentChannelMessageUpdateAction{ + socketModeClient: socketModeClient, + logger: logger, + incidentService: incidentService, + teamService: teamService, + severityService: severityService, + } +} + +func (icm *IncidentChannelMessageUpdateAction) ProcessAction(channelId string) { + incidentEntity, teamEntity, severityEntity, incidentChannels, incidentStatusEntity, err := icm.getEntities(channelId) + if err != nil { + return + } + + blocks := view.IncidentSummarySection(incidentEntity, teamEntity, severityEntity, incidentStatusEntity) + color := util.GetColorBySeverity(severityEntity.ID) + att := slack.Attachment{Blocks: blocks, Color: color} + for _, message := range *incidentChannels { + _, _, _, err := icm.socketModeClient.UpdateMessage(message.SlackChannel, message.MessageTimeStamp, slack.MsgOptionAttachments(att)) + if err != nil { + icm.logger.Error(fmt.Sprintf("exception occurred while updating the message to all the incident "+ + "channels for incidentId: %v", incidentEntity.ID), zap.Error(err)) + return + } + } +} + +func (icm *IncidentChannelMessageUpdateAction) getEntities(channelId string) (*incident.IncidentEntity, *team.TeamEntity, + *severity.SeverityEntity, *[]incident.IncidentChannelEntity, *incident.IncidentStatusEntity, error) { + incidentEntity, err := icm.incidentService.FindIncidentByChannelId(channelId) + if err != nil || incidentEntity == nil { + return nil, nil, nil, nil, nil, errors.New("exception occurred while getting incident") + } + + incidentChannels, err := icm.incidentService.GetIncidentChannels(incidentEntity.ID) + if err != nil || incidentChannels == nil { + return nil, nil, nil, nil, nil, errors.New("exception occurred while getting incident channels") + } + + teamEntity, err := icm.teamService.FindTeamById(incidentEntity.TeamId) + if err != nil || teamEntity == nil { + return nil, nil, nil, nil, nil, errors.New("exception occurred while getting incident team") + } + + severityEntity, err := icm.severityService.FindSeverityById(incidentEntity.SeverityId) + if err != nil || severityEntity == nil { + return nil, nil, nil, nil, nil, errors.New("exception occurred while getting incident severity") + } + + incidentStatusEntity, err := icm.incidentService.FindIncidentStatusById(incidentEntity.Status) + if err != nil || incidentStatusEntity == nil { + return nil, nil, nil, nil, nil, errors.New("exception occurred while getting incident status") + } + + return incidentEntity, teamEntity, severityEntity, incidentChannels, incidentStatusEntity, nil +} diff --git a/pkg/slack/houston/command/incident_resolve.go b/internal/processor/action/incident_resolve_action.go similarity index 51% rename from pkg/slack/houston/command/incident_resolve.go rename to internal/processor/action/incident_resolve_action.go index 6e566c0..cb483b0 100644 --- a/pkg/slack/houston/command/incident_resolve.go +++ b/internal/processor/action/incident_resolve_action.go @@ -1,57 +1,58 @@ -package command +package action import ( "fmt" - "houston/entity" - "houston/pkg/postgres/query" + "houston/pkg/postgres/service/incident" "time" "github.com/slack-go/slack" "github.com/slack-go/slack/socketmode" "go.uber.org/zap" - "gorm.io/gorm" ) -type incidentResolveProcessor struct { - client *socketmode.Client - db *gorm.DB - logger *zap.Logger +type ResolveIncidentAction struct { + client *socketmode.Client + logger *zap.Logger + incidentService *incident.Service } -func NewIncidentResolveProcessor(client *socketmode.Client, db *gorm.DB, logger *zap.Logger) *incidentResolveProcessor { - return &incidentResolveProcessor{ - client: client, - db: db, - logger: logger, +func NewIncidentResolveProcessor(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Service) *ResolveIncidentAction { + return &ResolveIncidentAction{ + client: client, + logger: logger, + incidentService: incidentService, } } -func (irp *incidentResolveProcessor) IncidentResolveProcess(callback slack.InteractionCallback, request *socketmode.Request) { +func (irp *ResolveIncidentAction) IncidentResolveProcess(callback slack.InteractionCallback, request *socketmode.Request) { channelId := callback.Channel.ID - incidentEntity, err := query.FindIncidentByChannelId(irp.db, channelId) + incidentEntity, err := irp.incidentService.FindIncidentByChannelId(channelId) if err != nil { irp.logger.Error("incident not found", zap.String("channel", channelId), zap.String("user_id", callback.User.ID), zap.Error(err)) } - incidentEntity.Status = entity.Resolved - now := time.Now() - incidentEntity.CustomerImpactEndTime = &now + incidentStatusEntity, _ := irp.incidentService.FindIncidentStatusByName(incident.Resolved) - err = query.UpdateIncident(irp.db, incidentEntity) + now := time.Now() + incidentEntity.Status = incidentStatusEntity.ID + incidentEntity.EndTime = &now + + err = irp.incidentService.UpdateIncident(incidentEntity) if err != nil { irp.logger.Error("failed to update incident to resolve state", zap.String("channel", channelId), zap.String("user_id", callback.User.ID), zap.Error(err)) + return } irp.logger.Info("successfully resolved the incident", zap.String("channel", channelId), zap.String("user_id", callback.User.ID)) - msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> > set status to %s", callback.User.ID, incidentEntity.Status), false) - + msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> > set status to %s", callback.User.ID, + incident.Resolved), false) _, _, errMessage := irp.client.PostMessage(callback.Channel.ID, msgOption) if errMessage != nil { irp.logger.Error("post response failed for ResolveIncident", zap.Error(errMessage)) diff --git a/internal/processor/action/incident_show_tags_action.go b/internal/processor/action/incident_show_tags_action.go new file mode 100644 index 0000000..a49f1d5 --- /dev/null +++ b/internal/processor/action/incident_show_tags_action.go @@ -0,0 +1,87 @@ +package action + +import ( + "fmt" + "houston/pkg/postgres/service/incident" + "houston/pkg/postgres/service/tag" + + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" +) + +type IncidentShowTagsAction struct { + client *socketmode.Client + logger *zap.Logger + incidentService *incident.Service + tagService *tag.Service +} + +func NewIncidentShowTagsProcessor(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Service, tagService *tag.Service) *IncidentShowTagsAction { + return &IncidentShowTagsAction{ + client: client, + logger: logger, + incidentService: incidentService, + tagService: tagService, + } +} + +func (isp *IncidentShowTagsAction) IncidentShowTagsRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { + incidentEntity, err := isp.incidentService.FindIncidentByChannelId(callback.Channel.ID) + if err != nil || incidentEntity == nil { + isp.logger.Error(fmt.Sprintf("failure while getting incident for channel: %v", callback.Channel.ID)) + return + } + + incidentTagsEntities, err := isp.incidentService.GetIncidentTagsByIncidentId(incidentEntity.ID) + if err != nil || incidentTagsEntities == nil { + isp.logger.Error(fmt.Sprintf("failure while getting incident tags for incident id: %v", incidentEntity.ID)) + return + } + + var msgStrings []string + + for _, incidentTagEntity := range *incidentTagsEntities { + tagEntity, err := isp.tagService.FindById(incidentTagEntity.TagId) + if err != nil || tagEntity == nil { + isp.logger.Error(fmt.Sprintf("failure while getting tags for incident id: %v", incidentEntity.ID)) + return + } + + if incidentTagEntity.FreeTextValue != nil && *incidentTagEntity.FreeTextValue != "" { + msgStrings = append(msgStrings, fmt.Sprintf("\n\n %v : \n %v", tagEntity.Label, *incidentTagEntity.FreeTextValue)) + } else if incidentTagEntity.TagValueIds != nil { + tagValues, err := isp.tagService.FindTagValuesByIds(incidentTagEntity.TagValueIds) + if err != nil { + isp.logger.Error(fmt.Sprintf("failure while getting tag values for incident id: %v", incidentEntity.ID)) + return + } else if tagValues == nil { + continue + } + var msg string + + for _, tv := range *tagValues { + if msg == "" { + msg = tv.Value + } else { + msg = msg + " " + tv.Value + } + } + msgStrings = append(msgStrings, fmt.Sprintf("\n\n %v : \n %v", tagEntity.Label, msg)) + } + } + + var finalMsg string + for _, msg := range msgStrings { + finalMsg = finalMsg + msg + } + + msgOption := slack.MsgOptionText(fmt.Sprintf(finalMsg), true) + _, errMessage := isp.client.PostEphemeral(callback.Channel.ID, callback.User.ID, msgOption) + if errMessage != nil { + isp.logger.Error("post ephemeral message response failed for IncidentShowTagsRequestProcess", zap.Error(errMessage)) + return + } + var payload interface{} + isp.client.Ack(*request, payload) +} diff --git a/pkg/slack/houston/command/incident_update_description.go b/internal/processor/action/incident_update_description_action.go similarity index 63% rename from pkg/slack/houston/command/incident_update_description.go rename to internal/processor/action/incident_update_description_action.go index d02878c..f99d51f 100644 --- a/pkg/slack/houston/command/incident_update_description.go +++ b/internal/processor/action/incident_update_description_action.go @@ -1,32 +1,30 @@ -package command +package action import ( "fmt" - "houston/pkg/postgres/query" - houston "houston/pkg/slack/houston/design" - "github.com/slack-go/slack" "github.com/slack-go/slack/socketmode" "go.uber.org/zap" - "gorm.io/gorm" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/incident" ) -type incidentUpdateDescriptionProcessor struct { - client *socketmode.Client - db *gorm.DB - logger *zap.Logger +type IncidentUpdateDescriptionAction struct { + client *socketmode.Client + logger *zap.Logger + incidentService *incident.Service } -func NewIncidentUpdateDescriptionProcessor(client *socketmode.Client, db *gorm.DB, logger *zap.Logger) *incidentUpdateDescriptionProcessor { - return &incidentUpdateDescriptionProcessor{ - client: client, - db: db, - logger: logger, +func NewIncidentUpdateDescriptionAction(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Service) *IncidentUpdateDescriptionAction { + return &IncidentUpdateDescriptionAction{ + client: client, + logger: logger, + incidentService: incidentService, } } -func (idp *incidentUpdateDescriptionProcessor) IncidentUpdateDescriptionRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { - result, err := query.FindIncidentByChannelId(idp.db, callback.Channel.ID) +func (idp *IncidentUpdateDescriptionAction) IncidentUpdateDescriptionRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { + result, err := idp.incidentService.FindIncidentByChannelId(callback.Channel.ID) if err != nil { idp.logger.Error("FindIncidentByChannelId error ", zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), @@ -38,11 +36,11 @@ func (idp *incidentUpdateDescriptionProcessor) IncidentUpdateDescriptionRequestP zap.String("user_id", callback.User.ID), zap.Error(err)) return } - modalRequest := houston.BuildIncidentUpdateDescriptionModal(idp.db, callback.Channel, result.Description) + modalRequest := view.BuildIncidentUpdateDescriptionModal(callback.Channel, result.Description) _, err = idp.client.OpenView(callback.TriggerID, modalRequest) if err != nil { - idp.logger.Error("houston slack openview command for IncidentUpdateDescriptionRequestProcess failed.", + idp.logger.Error("houston slackbot openview command for IncidentUpdateDescriptionRequestProcess failed.", zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) return } @@ -50,8 +48,8 @@ func (idp *incidentUpdateDescriptionProcessor) IncidentUpdateDescriptionRequestP idp.client.Ack(*request, payload) } -func (itp *incidentUpdateDescriptionProcessor) IncidentUpdateDescription(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { - incidentEntity, err := query.FindIncidentByChannelId(itp.db, callback.View.PrivateMetadata) +func (itp *IncidentUpdateDescriptionAction) IncidentUpdateDescription(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { + incidentEntity, err := itp.incidentService.FindIncidentByChannelId(callback.View.PrivateMetadata) if err != nil { itp.logger.Error("FindIncidentByChannelId error", zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), @@ -68,7 +66,7 @@ func (itp *incidentUpdateDescriptionProcessor) IncidentUpdateDescription(callbac incidentEntity.Description = incidentDescription incidentEntity.UpdatedBy = user.ID - err = query.UpdateIncident(itp.db, incidentEntity) + err = itp.incidentService.UpdateIncident(incidentEntity) if err != nil { itp.logger.Error("IncidentUpdateDescription error", zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), @@ -89,9 +87,9 @@ func (itp *incidentUpdateDescriptionProcessor) IncidentUpdateDescription(callbac func buildUpdateIncidentDescriptionRequest(blockActions map[string]map[string]slack.BlockAction) string { var requestMap = make(map[string]string, 0) for _, actions := range blockActions { - for actionID, action := range actions { - if action.Type == "plain_text_input" { - requestMap[actionID] = action.Value + for actionID, a := range actions { + if a.Type == "plain_text_input" { + requestMap[actionID] = a.Value } } } diff --git a/pkg/slack/houston/command/incident_update_severity.go b/internal/processor/action/incident_update_severity_action.go similarity index 55% rename from pkg/slack/houston/command/incident_update_severity.go rename to internal/processor/action/incident_update_severity_action.go index 1e6262d..8af3b4d 100644 --- a/pkg/slack/houston/command/incident_update_severity.go +++ b/internal/processor/action/incident_update_severity_action.go @@ -1,46 +1,50 @@ -package command +package action import ( "fmt" - "houston/pkg/postgres/query" - "houston/pkg/slack/common" - houston "houston/pkg/slack/houston/design" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/incident" + "houston/pkg/postgres/service/severity" + "houston/pkg/slackbot" "strconv" "time" "github.com/slack-go/slack" "github.com/slack-go/slack/socketmode" "go.uber.org/zap" - "gorm.io/gorm" ) -type incidentUpdateSevertityProcessor struct { - client *socketmode.Client - db *gorm.DB - logger *zap.Logger +type IncidentUpdateSevertityAction struct { + client *socketmode.Client + logger *zap.Logger + severityService *severity.Service + incidentService *incident.Service + slackbotClient *slackbot.Client } -func NewIncidentUpdateSeverityProcessor(client *socketmode.Client, db *gorm.DB, logger *zap.Logger) *incidentUpdateSevertityProcessor { - return &incidentUpdateSevertityProcessor{ - client: client, - db: db, - logger: logger, +func NewIncidentUpdateSeverityAction(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Service, severityService *severity.Service, slackbotClient *slackbot.Client) *IncidentUpdateSevertityAction { + return &IncidentUpdateSevertityAction{ + client: client, + logger: logger, + severityService: severityService, + incidentService: incidentService, + slackbotClient: slackbotClient, } } -func (isp *incidentUpdateSevertityProcessor) IncidentUpdateSeverityRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { - incidentSeverity, err := query.FindIncidentSeverityEntity(isp.db, isp.logger) - if err != nil { +func (isp *IncidentUpdateSevertityAction) IncidentUpdateSeverityRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { + incidentSeverity, err := isp.severityService.GetAllActiveSeverity() + if err != nil || incidentSeverity == nil { isp.logger.Error("FindSeverityEntity error", zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), zap.Error(err)) return } - modalRequest := houston.BuildIncidentUpdateSeverityModal(callback.Channel, incidentSeverity) + modalRequest := view.BuildIncidentUpdateSeverityModal(callback.Channel, *incidentSeverity) _, err = isp.client.OpenView(callback.TriggerID, modalRequest) if err != nil { - isp.logger.Error("houston slack openview command failed.", + isp.logger.Error("houston slackbot openview command failed.", zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) return } @@ -49,12 +53,13 @@ func (isp *incidentUpdateSevertityProcessor) IncidentUpdateSeverityRequestProces } -func (isp *incidentUpdateSevertityProcessor) IncidentUpdateSeverity(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { - incidentEntity, err := query.FindIncidentByChannelId(isp.db, callback.View.PrivateMetadata) +func (isp *IncidentUpdateSevertityAction) IncidentUpdateSeverity(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { + incidentEntity, err := isp.incidentService.FindIncidentByChannelId(callback.View.PrivateMetadata) if err != nil { isp.logger.Error("FindIncidentByChannelId error", zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), zap.String("user_id", user.ID), zap.Error(err)) + return } else if incidentEntity == nil { isp.logger.Error("IncidentEntity not found ", zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), @@ -63,38 +68,33 @@ func (isp *incidentUpdateSevertityProcessor) IncidentUpdateSeverity(callback sla } incidentSeverityId := buildUpdateIncidentSeverityRequest(isp.logger, callback.View.State.Values) - result, err := query.FindIncidentSeverityEntityById(isp.db, isp.logger, incidentSeverityId) + incidentSeverityEntity, err := isp.severityService.FindIncidentSeverityEntityById(incidentSeverityId) if err != nil { isp.logger.Error("FindIncidentSeverityEntityById error", zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), zap.String("user_id", user.ID), zap.Error(err)) return - } else if result == nil { + } else if incidentSeverityEntity == nil { isp.logger.Error("SeverityEntity not found", zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), zap.String("user_id", user.ID), zap.Error(err)) return } - incidentEntity.SeverityId = int(result.ID) + incidentEntity.SeverityId = incidentSeverityEntity.ID incidentEntity.UpdatedBy = user.ID - incidentEntity.SeverityTat = time.Now().AddDate(0, 0, result.Sla) - err = query.UpdateIncident(isp.db, incidentEntity) + incidentEntity.SeverityTat = time.Now().AddDate(0, 0, incidentSeverityEntity.Sla) + err = isp.incidentService.UpdateIncident(incidentEntity) if err != nil { isp.logger.Error("UpdateIncident error", zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), zap.String("user_id", user.ID), zap.Error(err)) } - userIdList, err := query.FindDefaultUserIdToBeAddedBySeverity(isp.db, int(result.ID)) - if err != nil { - isp.logger.Error("FindDefaultUserIdToBeAddedBySeverity error", - zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), - zap.String("user_id", user.ID), zap.Error(err)) - return + + for _, o := range incidentSeverityEntity.SlackUserIds { + isp.slackbotClient.InviteUsersToConversation(callback.View.PrivateMetadata, o) } - for _, o := range userIdList { - common.InviteUsersToConversation(isp.client, isp.logger, callback.View.PrivateMetadata, o) - } - msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> > set severity to %s", user.ID, result.Name), false) + + msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> > set severity to %s", user.ID, incidentSeverityEntity.Name), false) _, _, errMessage := isp.client.PostMessage(callback.View.PrivateMetadata, msgOption) if errMessage != nil { isp.logger.Error("post response failed for IncidentUpdateSeverity", zap.Error(errMessage)) @@ -104,14 +104,13 @@ func (isp *incidentUpdateSevertityProcessor) IncidentUpdateSeverity(callback sla isp.client.Ack(*request, payload) } -//TODO - ADD USER ACCORDING TO SEVERITY - +// TODO - ADD USER ACCORDING TO SEVERITY func buildUpdateIncidentSeverityRequest(logger *zap.Logger, blockActions map[string]map[string]slack.BlockAction) int { var requestMap = make(map[string]string, 0) for _, actions := range blockActions { - for actionID, action := range actions { - if action.Type == "static_select" { - requestMap[actionID] = action.SelectedOption.Value + for actionID, a := range actions { + if a.Type == "static_select" { + requestMap[actionID] = a.SelectedOption.Value } } } diff --git a/pkg/slack/houston/command/incident_update_status.go b/internal/processor/action/incident_update_status_action.go similarity index 57% rename from pkg/slack/houston/command/incident_update_status.go rename to internal/processor/action/incident_update_status_action.go index 6c16dbc..e37fe91 100644 --- a/pkg/slack/houston/command/incident_update_status.go +++ b/internal/processor/action/incident_update_status_action.go @@ -1,39 +1,43 @@ -package command +package action import ( "fmt" - "houston/entity" - "houston/pkg/postgres/query" - houston "houston/pkg/slack/houston/design" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/incident" "strconv" "time" "github.com/slack-go/slack" "github.com/slack-go/slack/socketmode" "go.uber.org/zap" - "gorm.io/gorm" ) -type incidentUpdateStatusProcessor struct { - client *socketmode.Client - db *gorm.DB - logger *zap.Logger +type UpdateIncidentAction struct { + client *socketmode.Client + logger *zap.Logger + incidentService *incident.Service } -func NewIncidentUpdateStatusProcessor(client *socketmode.Client, db *gorm.DB, logger *zap.Logger) *incidentUpdateStatusProcessor { - return &incidentUpdateStatusProcessor{ - client: client, - db: db, - logger: logger, +func NewIncidentUpdateAction(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Service) *UpdateIncidentAction { + return &UpdateIncidentAction{ + client: client, + logger: logger, + incidentService: incidentService, } } -func (isp *incidentUpdateStatusProcessor) IncidentUpdateStatusRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { - modalRequest := houston.BuildIncidentUpdateStatusModal(isp.db, callback.Channel) +func (isp *UpdateIncidentAction) IncidentUpdateStatusRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { + incidentStatuses, err := isp.incidentService.FetchAllIncidentStatuses() + if err != nil || incidentStatuses == nil { + isp.logger.Error("failed to get the all active incident statuses") + return + } - _, err := isp.client.OpenView(callback.TriggerID, modalRequest) + modalRequest := view.BuildIncidentUpdateStatusModal(*incidentStatuses, callback.Channel) + + _, err = isp.client.OpenView(callback.TriggerID, modalRequest) if err != nil { - isp.logger.Error("houston slack openview command failed.", + isp.logger.Error("houston slackbot openview command failed.", zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) return } @@ -41,8 +45,8 @@ func (isp *incidentUpdateStatusProcessor) IncidentUpdateStatusRequestProcess(cal isp.client.Ack(*request, payload) } -func (isp *incidentUpdateStatusProcessor) IncidentUpdateStatus(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { - incidentEntity, err := query.FindIncidentByChannelId(isp.db, callback.View.PrivateMetadata) +func (isp *UpdateIncidentAction) IncidentUpdateStatus(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { + incidentEntity, err := isp.incidentService.FindIncidentByChannelId(callback.View.PrivateMetadata) if err != nil { isp.logger.Error("FindIncidentBySlackChannelId error", zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), @@ -57,37 +61,39 @@ func (isp *incidentUpdateStatusProcessor) IncidentUpdateStatus(callback slack.In } incidentStatusId := buildUpdateIncidentStatusRequest(isp.logger, callback.View.State.Values) - result, err := query.FindIncidentStatusById(isp.db, incidentStatusId) + result, err := isp.incidentService.FindIncidentStatusById(incidentStatusId) if err != nil { isp.logger.Error("FindIncidentStatusById error", zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), zap.String("user_id", user.ID), zap.Error(err)) + return } else if result == nil { isp.logger.Error("IncidentStatusEntity Object not found", zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), zap.String("user_id", user.ID), zap.Error(err)) return } - incidentEntity.Status = entity.IncidentStatus(result.Name) + + incidentEntity.Status = result.ID incidentEntity.UpdatedBy = user.ID - if incidentEntity.Status == "RESOLVED" { + if result.IsTerminalStatus { now := time.Now() - incidentEntity.CustomerImpactEndTime = &now + incidentEntity.EndTime = &now } - err = query.UpdateIncident(isp.db, incidentEntity) + err = isp.incidentService.UpdateIncident(incidentEntity) if err != nil { isp.logger.Error("UpdateIncident error", zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), zap.String("user_id", user.ID), zap.Error(err)) } - msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> > set status to %s", user.ID, incidentEntity.Status), false) + msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> > set status to %s", user.ID, result.Name), false) _, _, errMessage := isp.client.PostMessage(callback.View.PrivateMetadata, msgOption) if errMessage != nil { isp.logger.Error("post response failed for IncidentUpdateStatus", zap.Error(errMessage)) return } - if incidentEntity.Status == "RESOLVED" { + if result.IsTerminalStatus { isp.client.ArchiveConversation(callback.View.PrivateMetadata) } @@ -95,14 +101,12 @@ func (isp *incidentUpdateStatusProcessor) IncidentUpdateStatus(callback slack.In isp.client.Ack(*request, payload) } -//TODO - FOR RESOLVED SCENARIO - -func buildUpdateIncidentStatusRequest(logger *zap.Logger, blockActions map[string]map[string]slack.BlockAction) int { +func buildUpdateIncidentStatusRequest(logger *zap.Logger, blockActions map[string]map[string]slack.BlockAction) uint { var requestMap = make(map[string]string, 0) for _, actions := range blockActions { - for actionID, action := range actions { - if action.Type == "static_select" { - requestMap[actionID] = action.SelectedOption.Value + for actionID, a := range actions { + if a.Type == "static_select" { + requestMap[actionID] = a.SelectedOption.Value } } } @@ -112,5 +116,5 @@ func buildUpdateIncidentStatusRequest(logger *zap.Logger, blockActions map[strin if err != nil { logger.Error("String conversion to int faileed in buildUpdateIncidentTypeRequest for "+selectedValue, zap.Error(err)) } - return selectedValueInInt + return uint(selectedValueInInt) } diff --git a/internal/processor/action/incident_update_tags_action.go b/internal/processor/action/incident_update_tags_action.go new file mode 100644 index 0000000..66c3cdd --- /dev/null +++ b/internal/processor/action/incident_update_tags_action.go @@ -0,0 +1,191 @@ +package action + +import ( + "fmt" + "github.com/lib/pq" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/incident" + "houston/pkg/postgres/service/tag" + "houston/pkg/postgres/service/team" + "strconv" + + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" +) + +type IncidentUpdateTagsAction struct { + client *socketmode.Client + logger *zap.Logger + incidentService *incident.Service + teamService *team.Service + tagService *tag.Service +} + +func NewIncidentUpdateTagsAction(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Service, + teamService *team.Service, tagService *tag.Service) *IncidentUpdateTagsAction { + return &IncidentUpdateTagsAction{ + client: client, + logger: logger, + incidentService: incidentService, + teamService: teamService, + tagService: tagService, + } +} + +func (itp *IncidentUpdateTagsAction) IncidentUpdateTagsRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { + incidentEntity, err := itp.incidentService.FindIncidentByChannelId(callback.Channel.ID) + if err != nil || incidentEntity == nil { + itp.logger.Error(fmt.Sprintf("failure while getting incident entity for channel: %v", callback.Channel.ID)) + return + } + + team, err := itp.teamService.FindTeamById(incidentEntity.TeamId) + if err != nil || team == nil { + itp.logger.Error(fmt.Sprintf("failure while getting team for incident id: %v", incidentEntity.ID)) + return + } + + tags, err := itp.tagService.FindTagsByTeamId(team.ID) + if err != nil || tags == nil { + itp.logger.Error(fmt.Sprintf("failure while getting tags for incident id: %v", incidentEntity.ID)) + return + } + + var blocks []slack.InputBlock + + for _, t := range *tags { + tagValues, err := itp.tagService.FindTagValuesByTagId(t.Id) + if err != nil { + itp.logger.Error(fmt.Sprintf("failed to get the tag values for tagId: %v", t.Id)) + return + } + + incidentTag, err := itp.incidentService.GetIncidentTagByTagId(incidentEntity.ID, t.Id) + if err != nil { + itp.logger.Error(fmt.Sprintf("failed to get the incident tag for incidentId: %v", incidentEntity.ID)) + return + } + + var block *slack.InputBlock + + if incidentTag == nil { + incidentTag, err = itp.incidentService.CreateIncidentTag(incidentEntity.ID, t.Id) + if err != nil || incidentTag == nil { + itp.logger.Error(fmt.Sprintf("failure while creating tag for incident id: %v", incidentEntity.ID)) + return + } + } + + if t.Type == tag.FreeText { + var initialValue string + if incidentTag.FreeTextValue == nil { + initialValue = "" + } else { + initialValue = *incidentTag.FreeTextValue + } + block = view.CreateInputBlock(t, nil, initialValue, t.Optional) + } else { + var initialTags []tag.TagValueEntity + + if tagValues == nil { + itp.logger.Error(fmt.Sprintf("no tag values are present for tag: %v", t.Id)) + return + } + + if incidentTag.TagValueIds != nil { + for _, tv := range *tagValues { + for _, it := range incidentTag.TagValueIds { + if tv.ID == uint(it) { + ltv := tv + initialTags = append(initialTags, ltv) + } + } + } + } + + block = view.CreateInputBlock(t, *tagValues, initialTags, t.Optional) + } + + blocks = append(blocks, *block) + } + + modalRequest := view.BuildIncidentUpdateTagModal(callback.Channel, blocks) + + _, err = itp.client.OpenView(callback.TriggerID, modalRequest) + if err != nil { + itp.logger.Error("houston slackbot openview command for IncidentUpdateTagsRequestProcess failed.", + zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) + return + } + var payload interface{} + itp.client.Ack(*request, payload) +} + +func (itp *IncidentUpdateTagsAction) IncidentUpdateTags(callback slack.InteractionCallback, request *socketmode.Request) { + incidentEntity, err := itp.incidentService.FindIncidentByChannelId(callback.View.PrivateMetadata) + if err != nil || incidentEntity == nil { + itp.logger.Error(fmt.Sprintf("failed to get the incicent for channel id: %v", callback.View.PrivateMetadata)) + return + } + + incidentTagsEntity, err := itp.incidentService.GetIncidentTagsByIncidentId(incidentEntity.ID) + if err != nil || incidentTagsEntity == nil { + itp.logger.Error(fmt.Sprintf("failed to get the incicent tags for incident id: %v", incidentEntity.ID)) + return + } + + blockActions := callback.View.State.Values + actions := make(map[string]slack.BlockAction, 0) + + for _, a := range blockActions { + for key, value := range a { + actions[key] = value + } + } + + // build request to update the tag values + for _, it := range *incidentTagsEntity { + tagEntity, err := itp.tagService.FindById(it.TagId) + if err != nil || tagEntity == nil { + itp.logger.Error(fmt.Sprintf("failed to get the tag for id: %v", it.TagId)) + return + } + + if tagEntity.Type == tag.FreeText { + localValue := actions[tagEntity.ActionId].Value + it.FreeTextValue = &localValue + } else if tagEntity.Type == tag.SingleValue { + localValue := actions[tagEntity.ActionId].SelectedOption.Value + value, err := strconv.Atoi(localValue) + if err != nil { + itp.logger.Error(fmt.Sprintf("string to int conversion failed for incident: %v, tag value: %v", + incidentEntity.ID, localValue)) + return + } + it.TagValueIds = pq.Int32Array{int32(value)} + } else if tagEntity.Type == tag.MultiValue { + var valueArray pq.Int32Array + for _, o := range actions[tagEntity.ActionId].SelectedOptions { + localValue, err := strconv.Atoi(o.Value) + if err != nil { + itp.logger.Error(fmt.Sprintf("string to int conversion failed for incident: %v, tag value: %v", + incidentEntity.ID, localValue)) + return + } + valueArray = append(valueArray, int32(localValue)) + } + it.TagValueIds = valueArray + } + + _, err = itp.incidentService.SaveIncidentTag(it) + if err != nil { + itp.logger.Error(fmt.Sprintf("Failed while saving incident tag values for incidentId: %v", + callback.View.PrivateMetadata), zap.Error(err)) + return + } + } + + var payload interface{} + itp.client.Ack(*request, payload) +} diff --git a/pkg/slack/houston/command/incident_update_title.go b/internal/processor/action/incident_update_title_action.go similarity index 64% rename from pkg/slack/houston/command/incident_update_title.go rename to internal/processor/action/incident_update_title_action.go index 28e3d61..324871d 100644 --- a/pkg/slack/houston/command/incident_update_title.go +++ b/internal/processor/action/incident_update_title_action.go @@ -1,32 +1,30 @@ -package command +package action import ( "fmt" - "houston/pkg/postgres/query" - houston "houston/pkg/slack/houston/design" - "github.com/slack-go/slack" "github.com/slack-go/slack/socketmode" "go.uber.org/zap" - "gorm.io/gorm" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/incident" ) -type incidentUpdateTitleProcessor struct { - client *socketmode.Client - db *gorm.DB - logger *zap.Logger +type IncidentUpdateTitleAction struct { + client *socketmode.Client + logger *zap.Logger + incidentService *incident.Service } -func NewIncidentUpdateTitleProcessor(client *socketmode.Client, db *gorm.DB, logger *zap.Logger) *incidentUpdateTitleProcessor { - return &incidentUpdateTitleProcessor{ - client: client, - db: db, - logger: logger, +func NewIncidentUpdateTitleAction(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Service) *IncidentUpdateTitleAction { + return &IncidentUpdateTitleAction{ + client: client, + logger: logger, + incidentService: incidentService, } } -func (itp *incidentUpdateTitleProcessor) IncidentUpdateTitleRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { - result, err := query.FindIncidentByChannelId(itp.db, callback.Channel.ID) +func (itp *IncidentUpdateTitleAction) IncidentUpdateTitleRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { + result, err := itp.incidentService.FindIncidentByChannelId(callback.Channel.ID) if err != nil { itp.logger.Error("FindIncidentByChannelId error", zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), @@ -38,11 +36,11 @@ func (itp *incidentUpdateTitleProcessor) IncidentUpdateTitleRequestProcess(callb zap.String("user_id", callback.User.ID), zap.Error(err)) return } - modalRequest := houston.BuildIncidentUpdateTitleModal(itp.db, callback.Channel, result.Title) + modalRequest := view.BuildIncidentUpdateTitleModal(callback.Channel, result.Title) _, err = itp.client.OpenView(callback.TriggerID, modalRequest) if err != nil { - itp.logger.Error("houston slack openview command for IncidentUpdateTitleRequestProcess failed.", + itp.logger.Error("houston slackbot openview command for IncidentUpdateTitleRequestProcess failed.", zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) return } @@ -50,8 +48,8 @@ func (itp *incidentUpdateTitleProcessor) IncidentUpdateTitleRequestProcess(callb itp.client.Ack(*request, payload) } -func (itp *incidentUpdateTitleProcessor) IncidentUpdateTitle(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { - incidentEntity, err := query.FindIncidentByChannelId(itp.db, callback.View.PrivateMetadata) +func (itp *IncidentUpdateTitleAction) IncidentUpdateTitle(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { + incidentEntity, err := itp.incidentService.FindIncidentByChannelId(callback.View.PrivateMetadata) if err != nil { itp.logger.Error("FindIncidentByChannelId error", zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), @@ -68,7 +66,7 @@ func (itp *incidentUpdateTitleProcessor) IncidentUpdateTitle(callback slack.Inte incidentEntity.Title = incidentTitle incidentEntity.UpdatedBy = user.ID - err = query.UpdateIncident(itp.db, incidentEntity) + err = itp.incidentService.UpdateIncident(incidentEntity) if err != nil { itp.logger.Error("IncidentUpdateTitle error", zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), @@ -82,12 +80,12 @@ func (itp *incidentUpdateTitleProcessor) IncidentUpdateTitle(callback slack.Inte return } - result, err := query.FindIncidentSeverityTeamJoin(itp.db, incidentEntity.SlackChannel) + result, err := itp.incidentService.FindIncidentSeverityTeamJoin(incidentEntity.SlackChannel) if err != nil { itp.logger.Error("query failed for FindIncidentSeverityTeamJoin", zap.Error(errMessage)) return } - msgOption = slack.MsgOptionText(fmt.Sprintf("set the channel topic: %s : %s %s | %s", result.TeamsName, result.SeverityName, incidentEntity.IncidentName, incidentEntity.Title), false) + msgOption = slack.MsgOptionText(fmt.Sprintf("set the channel topic: %s : %s %s | %s", result.TeamName, result.SeverityName, incidentEntity.IncidentName, incidentEntity.Title), false) _, _, errMessage = itp.client.PostMessage(callback.View.PrivateMetadata, msgOption) if errMessage != nil { itp.logger.Error("post response failed for IncidentUpdateTitle", zap.Error(errMessage)) @@ -100,9 +98,9 @@ func (itp *incidentUpdateTitleProcessor) IncidentUpdateTitle(callback slack.Inte func buildUpdateIncidentTitleRequest(blockActions map[string]map[string]slack.BlockAction) string { var requestMap = make(map[string]string, 0) for _, actions := range blockActions { - for actionID, action := range actions { - if action.Type == "plain_text_input" { - requestMap[actionID] = action.Value + for actionID, a := range actions { + if a.Type == "plain_text_input" { + requestMap[actionID] = a.Value } } } diff --git a/internal/processor/action/incident_update_type_action.go b/internal/processor/action/incident_update_type_action.go new file mode 100644 index 0000000..237e252 --- /dev/null +++ b/internal/processor/action/incident_update_type_action.go @@ -0,0 +1,147 @@ +package action + +import ( + "fmt" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/incident" + "houston/pkg/postgres/service/team" + "houston/pkg/slackbot" + "strconv" + "time" + + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" +) + +type IncidentUpdateTypeAction struct { + socketModeClient *socketmode.Client + logger *zap.Logger + teamService *team.Service + incidentService *incident.Service + slackbotClient *slackbot.Client +} + +func NewIncidentUpdateTypeAction(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Service, teamService *team.Service, slackbotClient *slackbot.Client) *IncidentUpdateTypeAction { + return &IncidentUpdateTypeAction{ + socketModeClient: client, + logger: logger, + teamService: teamService, + incidentService: incidentService, + slackbotClient: slackbotClient, + } +} + +func (itp *IncidentUpdateTypeAction) IncidentUpdateTypeRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { + teams, err := itp.teamService.GetAllActiveTeams() + if err != nil { + itp.logger.Error("GetAllActiveTeams error", + zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), + zap.String("user_id", callback.User.ID), zap.Error(err)) + return + } + + modalRequest := view.BuildIncidentUpdateTypeModal(callback.Channel, *teams) + + _, err = itp.socketModeClient.OpenView(callback.TriggerID, modalRequest) + if err != nil { + itp.logger.Error("houston slackbot openview command failed.", + zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) + return + } + var payload interface{} + itp.socketModeClient.Ack(*request, payload) +} + +func (itp *IncidentUpdateTypeAction) IncidentUpdateType(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { + incidentEntity, err := itp.incidentService.FindIncidentByChannelId(callback.View.PrivateMetadata) + if err != nil { + itp.logger.Error("FindIncidentByChannelId error", + zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), + zap.String("user_id", user.ID), zap.Error(err)) + return + } else if incidentEntity == nil { + itp.logger.Error("IncidentEntity not found ", + zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), + zap.String("user_id", callback.User.ID), zap.Error(err)) + return + } + + incidentTypeId := itp.buildUpdateIncidentTypeRequest(callback.View.State.Values) + teamEntity, err := itp.teamService.FindTeamById(incidentTypeId) + if err != nil { + itp.logger.Error("FindTeamEntityById error", + zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), + zap.String("user_id", user.ID), zap.Error(err)) + return + } else if teamEntity == nil { + itp.logger.Error("Team Not Found", + zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), + zap.String("user_id", user.ID), zap.Error(err)) + return + } + incidentEntity.TeamId = teamEntity.ID + incidentEntity.UpdatedBy = user.ID + err = itp.incidentService.UpdateIncident(incidentEntity) + if err != nil { + itp.logger.Error("UpdateIncident error", + zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), + zap.String("user_id", user.ID), zap.Error(err)) + } + + itp.addDefaultUsersToIncident(callback.View.PrivateMetadata, incidentEntity.TeamId) + + msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> > set Team to %s", user.ID, teamEntity.Name), false) + _, _, errMessage := itp.socketModeClient.PostMessage(callback.View.PrivateMetadata, msgOption) + if errMessage != nil { + itp.logger.Error("post response failed for IncidentUpdateType", zap.Error(errMessage)) + return + } + var payload interface{} + itp.socketModeClient.Ack(*request, payload) +} + +func (itp *IncidentUpdateTypeAction) addDefaultUsersToIncident(channelId string, teamId uint) error { + team, _ := itp.teamService.FindTeamById(teamId) + + userIdList := team.SlackUserIds + + for _, o := range userIdList { + itp.slackbotClient.InviteUsersToConversation(channelId, o) + } + return nil +} + +func (itp *IncidentUpdateTypeAction) InviteOnCallPersonToIncident(channelId, ts string) { + go func() { + time.Sleep(3 * time.Second) + msg, _, _, _ := itp.socketModeClient.GetConversationReplies(&slack.GetConversationRepliesParameters{ + ChannelID: channelId, + Timestamp: ts, + Limit: 2, + }, + ) + if len(msg) > 1 { + //User id needs to sliced from `<@XXXXXXXXXXXX>` format to `XXXXXXXXXXXX` + itp.socketModeClient.InviteUsersToConversation(channelId, msg[1].Text[2:13]) + } + }() +} + +func (itp *IncidentUpdateTypeAction) buildUpdateIncidentTypeRequest(blockActions map[string]map[string]slack.BlockAction) uint { + var requestMap = make(map[string]string, 0) + for _, actions := range blockActions { + for actionID, a := range actions { + if a.Type == "static_select" { + requestMap[actionID] = a.SelectedOption.Value + } + } + } + + selectedValue := requestMap["incident_type_modal_request"] + selectedValueInInt, err := strconv.Atoi(selectedValue) + if err != nil { + itp.logger.Error("String conversion to int faileed in buildUpdateIncidentTypeRequest for "+selectedValue, zap.Error(err)) + } + return uint(selectedValueInInt) +} diff --git a/internal/processor/action/member_join_action.go b/internal/processor/action/member_join_action.go new file mode 100644 index 0000000..2bd01e0 --- /dev/null +++ b/internal/processor/action/member_join_action.go @@ -0,0 +1,85 @@ +package action + +import ( + "github.com/slack-go/slack/slackevents" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/incident" + "houston/pkg/postgres/service/severity" + "houston/pkg/postgres/service/team" +) + +type MemberJoinAction struct { + client *socketmode.Client + logger *zap.Logger + incidentService *incident.Service + teamService *team.Service + severityService *severity.Service +} + +func NewMemberJoinAction(socketModeClient *socketmode.Client, logger *zap.Logger, incidentService *incident.Service, teamService *team.Service, severityService *severity.Service) *MemberJoinAction { + return &MemberJoinAction{ + client: socketModeClient, + logger: logger, + incidentService: incidentService, + teamService: teamService, + severityService: severityService, + } +} + +func (mp *MemberJoinAction) PerformAction(memberJoinedChannelEvent *slackevents.MemberJoinedChannelEvent) { + mp.logger.Info("processing member join action", zap.String("channel", memberJoinedChannelEvent.Channel)) + + incidentEntity, err := mp.incidentService.FindIncidentByChannelId(memberJoinedChannelEvent.Channel) + if err != nil { + mp.logger.Error("error in searching incident", zap.String("channel", memberJoinedChannelEvent.Channel), + zap.String("user_id", memberJoinedChannelEvent.User), zap.Error(err)) + return + } else if err == nil && incidentEntity == nil { + mp.logger.Info("incident not found", zap.String("channel", memberJoinedChannelEvent.Channel), + zap.String("user_id", memberJoinedChannelEvent.User), zap.Error(err)) + return + } + + teamEntity, err := mp.teamService.FindTeamById(incidentEntity.TeamId) + if err != nil { + mp.logger.Error("error in fetching team", zap.String("channel", memberJoinedChannelEvent.Channel), + zap.Uint("incident_id", incidentEntity.ID), zap.Error(err)) + return + } else if teamEntity == nil { + mp.logger.Info("team not found", zap.String("channel", memberJoinedChannelEvent.Channel), + zap.Uint("incident_id", incidentEntity.ID)) + return + } + + severityEntity, err := mp.severityService.FindSeverityById(incidentEntity.SeverityId) + if err != nil { + mp.logger.Error("error in fetching severity", zap.String("channel", memberJoinedChannelEvent.Channel), + zap.Uint("incident_id", incidentEntity.ID), zap.Error(err)) + return + } else if severityEntity == nil { + mp.logger.Info("severity not found", zap.String("channel", memberJoinedChannelEvent.Channel), + zap.Uint("incident_id", incidentEntity.ID)) + return + } + + incidentStatusEntity, err := mp.incidentService.FindIncidentStatusById(incidentEntity.Status) + if err != nil { + mp.logger.Error("error in fetching incident status", zap.String("channel", memberJoinedChannelEvent.Channel), + zap.Uint("incident_id", incidentEntity.ID), zap.Error(err)) + return + } else if incidentStatusEntity == nil { + mp.logger.Info("incident status not found", zap.String("channel", memberJoinedChannelEvent.Channel), + zap.Uint("incident_id", incidentEntity.ID)) + return + } + + blocks := view.IncidentSummarySection(incidentEntity, teamEntity, severityEntity, incidentStatusEntity) + msgOption := view.IncidentEphemeralMessage(incidentEntity, blocks) + + _, err = mp.client.PostEphemeral(memberJoinedChannelEvent.Channel, memberJoinedChannelEvent.User, msgOption) + if err != nil { + mp.logger.Error("post response failed", zap.Error(err)) + } +} diff --git a/internal/processor/action/show_incidents_action.go b/internal/processor/action/show_incidents_action.go new file mode 100644 index 0000000..82205d6 --- /dev/null +++ b/internal/processor/action/show_incidents_action.go @@ -0,0 +1,46 @@ +package action + +import ( + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "github.com/spf13/viper" + "go.uber.org/zap" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/incident" +) + +type ShowIncidentsAction struct { + client *socketmode.Client + logger *zap.Logger + incidentService *incident.Service +} + +func ShowIncidentsProcessor(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Service) *ShowIncidentsAction { + return &ShowIncidentsAction{ + client: client, + logger: logger, + incidentService: incidentService, + } +} + +func (sip *ShowIncidentsAction) ProcessAction(channel slack.Channel, user slack.User, triggerId string, request *socketmode.Request) { + + limit := viper.GetInt("SHOW_INCIDENTS_LIMIT") + s, err := sip.incidentService.GetOpenIncidents(limit) + if err != nil { + sip.logger.Error("GetOpenIncidents query failed.", + zap.String("trigger_id", triggerId), zap.String("channel_id", channel.ID), zap.Error(err)) + return + } + + blocks := view.GenerateModalForShowIncidentsButtonSection(*s) + msgOption := slack.MsgOptionBlocks(blocks...) + _, err = sip.client.Client.PostEphemeral(channel.ID, user.ID, msgOption) + if err != nil { + sip.logger.Error("houston slackbot PostEphemeral command failed for ProcessAction.", + zap.String("trigger_id", triggerId), zap.String("channel_id", channel.ID), zap.String("user_id", user.ID), zap.Error(err)) + return + } + var payload interface{} + sip.client.Ack(*request, payload) +} diff --git a/internal/processor/action/slash_command_action.go b/internal/processor/action/slash_command_action.go new file mode 100644 index 0000000..e1982e6 --- /dev/null +++ b/internal/processor/action/slash_command_action.go @@ -0,0 +1,49 @@ +package action + +import ( + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/incident" +) + +type SlashCommandAction struct { + incidentService *incident.Service + logger *zap.Logger + socketModeClient *socketmode.Client +} + +func NewSlashCommandAction(service *incident.Service, logger *zap.Logger, socketModeClient *socketmode.Client) *SlashCommandAction { + return &SlashCommandAction{ + incidentService: service, + logger: logger, + socketModeClient: socketModeClient, + } +} + +func (sca *SlashCommandAction) PerformAction(evt *socketmode.Event) { + cmd, ok := evt.Data.(slack.SlashCommand) + sca.logger.Info("processing houston command", zap.Any("payload", cmd)) + if !ok { + sca.logger.Error("event data to slash command conversion failed", zap.Any("data", evt)) + return + } + + result, err := sca.incidentService.FindIncidentByChannelId(cmd.ChannelID) + if err != nil { + sca.logger.Error("FindIncidentBySlackChannelId errors", + zap.String("channel_id", cmd.ChannelID), zap.String("channel", cmd.ChannelName), + zap.String("user_id", cmd.UserID), zap.Error(err)) + return + } + + if result != nil { + sca.logger.Info("Result", zap.String("result", result.IncidentName)) + payload := view.ExistingIncidentOptionsBlock() + sca.socketModeClient.Ack(*evt.Request, payload) + } + + payload := view.NewIncidentBlock() + sca.socketModeClient.Ack(*evt.Request, payload) +} diff --git a/internal/processor/action/start_incident_block_action.go b/internal/processor/action/start_incident_block_action.go new file mode 100644 index 0000000..ca8ab9e --- /dev/null +++ b/internal/processor/action/start_incident_block_action.go @@ -0,0 +1,53 @@ +package action + +import ( + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/severity" + "houston/pkg/postgres/service/team" +) + +type StartIncidentBlockAction struct { + socketModeClient *socketmode.Client + logger *zap.Logger + teamService *team.Service + severityService *severity.Service +} + +func NewStartIncidentBlockAction(client *socketmode.Client, logger *zap.Logger, teamService *team.Service, severityService *severity.Service) *StartIncidentBlockAction { + return &StartIncidentBlockAction{ + socketModeClient: client, + logger: logger, + teamService: teamService, + severityService: severityService, + } +} + +func (sip *StartIncidentBlockAction) ProcessAction(request *socketmode.Request, callback slack.InteractionCallback) { + teams, err := sip.teamService.GetAllActiveTeams() + if err != nil || teams == nil { + sip.logger.Error("[SIP] failed while getting all active teams") + return + } + + severities, err := sip.severityService.GetAllActiveSeverity() + if err != nil || severities == nil { + sip.logger.Error("[SIP] failed while getting all active severities") + return + } + + modal := view.GenerateModalRequest(*teams, *severities, callback.Channel.ID) + + _, err = sip.socketModeClient.OpenView(callback.TriggerID, modal) + if err != nil { + sip.logger.Error("[SIP] houston slackbot open view command failed.", + zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) + return + } + + sip.logger.Info("[SIP] houston successfully send modal to slackbot", zap.String("trigger_id", callback.TriggerID)) + var payload interface{} + sip.socketModeClient.Ack(*request, payload) +} diff --git a/internal/processor/action/start_incident_modal_submission_action.go b/internal/processor/action/start_incident_modal_submission_action.go new file mode 100644 index 0000000..c4aa5c8 --- /dev/null +++ b/internal/processor/action/start_incident_modal_submission_action.go @@ -0,0 +1,208 @@ +package action + +import ( + "encoding/json" + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/common/util" + "houston/internal/processor/action/view" + "houston/pkg/postgres/service/incident" + "houston/pkg/postgres/service/severity" + "houston/pkg/postgres/service/team" + "houston/pkg/slackbot" + "time" +) + +type CreateIncidentAction struct { + client *socketmode.Client + logger *zap.Logger + incidentService *incident.Service + slackbotClient *slackbot.Client + teamService *team.Service + severityService *severity.Service +} + +func NewCreateIncidentProcessor(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Service, + teamService *team.Service, severityService *severity.Service, slackbotClient *slackbot.Client) *CreateIncidentAction { + return &CreateIncidentAction{ + client: client, + logger: logger, + incidentService: incidentService, + teamService: teamService, + severityService: severityService, + slackbotClient: slackbotClient, + } +} + +func (cip *CreateIncidentAction) CreateIncidentModalCommandProcessing(callback slack.InteractionCallback, request *socketmode.Request) { + // Build create incident request + createIncidentRequest := buildCreateIncidentRequest(callback) + cip.logger.Info("[CIP] incident request created", zap.Any("request", createIncidentRequest)) + + // Save the incident to the database + incidentEntity, err := cip.incidentService.CreateIncident(createIncidentRequest) + if err != nil { + cip.logger.Error("[CIP] Error while creating incident", zap.Error(err)) + return + } + + channelID, err := cip.createSlackChannel(incidentEntity) + if err != nil { + cip.logger.Error("[CIP] Error while creating incident channel", zap.Error(err)) + return + } + + teamEntity, severityEntity, incidentStatusEntity, err := cip.getTeamAndSeverityAndStatus(incidentEntity.TeamId, + incidentEntity.SeverityId, incidentEntity.Status) + if err != nil { + cip.logger.Error("[CIP] failed whilte getting team, severity and status", zap.Error(err)) + return + } + + // Post incident summary to Blaze Group channel and incident channel + timestamp, err := cip.postIncidentSummary(callback.View.PrivateMetadata, *channelID, incidentEntity, teamEntity, + severityEntity, incidentStatusEntity) + if err != nil { + cip.logger.Error("[CIP] error while posting incident summary", zap.Error(err)) + return + } + + // add default users to the incident + err = cip.addDefaultUsersToIncident(*channelID, teamEntity, severityEntity) + if err != nil { + cip.logger.Error("[CIP] error while adding default users to incident", zap.Error(err)) + return + } + + cip.InviteOnCallPersonToIncident(*channelID, *timestamp) + + // Acknowledge the interaction callback + var payload interface{} + cip.client.Ack(*request, payload) +} + +func (cip *CreateIncidentAction) InviteOnCallPersonToIncident(channelId, ts string) { + go func() { + time.Sleep(3 * time.Second) + msg, _, _, _ := cip.client.GetConversationReplies(&slack.GetConversationRepliesParameters{ + ChannelID: channelId, + Timestamp: ts, + Limit: 2, + }, + ) + if len(msg) > 1 { + //User id needs to sliced from `<@XXXXXXXXXXXX>` format to `XXXXXXXXXXXX` + cip.client.InviteUsersToConversation(channelId, msg[1].Text[2:13]) + } + }() +} + +func (cip *CreateIncidentAction) createSlackChannel(incidentEntity *incident.IncidentEntity) (*string, error) { + channelName := fmt.Sprintf("houston-%d", incidentEntity.ID) + channelID, err := cip.slackbotClient.CreateChannel(channelName) + if err != nil { + return nil, err + } + + incidentEntity.SlackChannel = channelID + incidentEntity.IncidentName = channelName + err = cip.incidentService.UpdateIncident(incidentEntity) + if err != nil { + cip.logger.Error(fmt.Sprintf("[CIP] failed to update the slack channel name for incident-id: %v", incidentEntity.ID)) + return nil, err + } + return &channelID, nil +} + +func (cip *CreateIncidentAction) addDefaultUsersToIncident(channelId string, teamEntity *team.TeamEntity, + severityEntity *severity.SeverityEntity) error { + var userIdList []string + userIdList = append(append(userIdList, teamEntity.SlackUserIds...), severityEntity.SlackUserIds...) + + for _, o := range userIdList { + cip.slackbotClient.InviteUsersToConversation(channelId, o) + } + return nil +} + +func (cip *CreateIncidentAction) postIncidentSummary(blazeGroupChannelID, incidentChannelID string, + incidentEntity *incident.IncidentEntity, teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity, + incidentStatusEntity *incident.IncidentStatusEntity) (*string, error) { + // Post incident summary to Blaze Group channel and incident channel + blocks := view.IncidentSummarySection(incidentEntity, teamEntity, severityEntity, incidentStatusEntity) + color := util.GetColorBySeverity(incidentEntity.SeverityId) + att := slack.Attachment{Blocks: blocks, Color: color} + _, timestamp, err := cip.client.PostMessage(blazeGroupChannelID, slack.MsgOptionAttachments(att)) + + if err == nil { + cip.incidentService.CreateIncidentChannelEntry(&incident.CreateIncidentChannelEntry{ + SlackChannel: blazeGroupChannelID, + MessageTimeStamp: timestamp, + IncidentId: incidentEntity.ID, + }) + } else { + return nil, fmt.Errorf("[CIP] error in saving message %v", err) + } + + _, timestamp, err = cip.client.PostMessage(incidentChannelID, slack.MsgOptionAttachments(att)) + if err == nil { + cip.incidentService.CreateIncidentChannelEntry(&incident.CreateIncidentChannelEntry{ + SlackChannel: incidentChannelID, + MessageTimeStamp: timestamp, + IncidentId: incidentEntity.ID, + }) + } else { + return nil, fmt.Errorf("[CIP] error in saving message %v", err) + } + return ×tamp, nil +} + +func (cip *CreateIncidentAction) getTeamAndSeverityAndStatus(teamId, severityId, status uint) (*team.TeamEntity, + *severity.SeverityEntity, *incident.IncidentStatusEntity, error) { + teamEntity, err := cip.teamService.FindTeamById(teamId) + if err != nil || teamEntity == nil { + return nil, nil, nil, err + } + + severityEntity, err := cip.severityService.FindSeverityById(severityId) + if err != nil || severityEntity == nil { + return nil, nil, nil, err + } + + incidentStatusEntity, err := cip.incidentService.FindIncidentStatusById(status) + if err != nil || incidentStatusEntity == nil { + return nil, nil, nil, err + } + + return teamEntity, severityEntity, incidentStatusEntity, nil +} + +func buildCreateIncidentRequest(callback slack.InteractionCallback) *incident.CreateIncidentRequest { + blockActions := callback.View.State.Values + var createIncidentRequest incident.CreateIncidentRequest + var requestMap = make(map[string]string, 0) + + for _, actions := range blockActions { + for actionID, a := range actions { + if string(a.Type) == string(slack.METPlainTextInput) { + requestMap[actionID] = a.Value + } + if string(a.Type) == slack.OptTypeStatic { + requestMap[actionID] = a.SelectedOption.Value + } + } + } + + desRequestMap, _ := json.Marshal(requestMap) + json.Unmarshal(desRequestMap, &createIncidentRequest) + + createIncidentRequest.Status = incident.Investigating + createIncidentRequest.StartTime = time.Now() + createIncidentRequest.EnableReminder = false + createIncidentRequest.CreatedBy = callback.User.ID + createIncidentRequest.UpdatedBy = callback.User.ID + + return &createIncidentRequest +} diff --git a/internal/processor/action/view/create_incident_modal.go b/internal/processor/action/view/create_incident_modal.go new file mode 100644 index 0000000..cd5660e --- /dev/null +++ b/internal/processor/action/view/create_incident_modal.go @@ -0,0 +1,91 @@ +package view + +import ( + "houston/common/util" + "houston/pkg/postgres/service/severity" + "houston/pkg/postgres/service/team" + "strconv" + + "github.com/slack-go/slack" +) + +func GenerateModalRequest(teams []team.TeamEntity, severities []severity.SeverityEntity, channel string) slack.ModalViewRequest { + // Create a ModalViewRequest with a header and two inputs + titleText := slack.NewTextBlockObject(slack.PlainTextType, "Houston", false, false) + closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) + submitText := slack.NewTextBlockObject(slack.PlainTextType, "Send", false, false) + + headerText := slack.NewTextBlockObject("mrkdwn", "Start Incident", false, false) + headerSection := slack.NewSectionBlock(headerText, nil, nil) + + incidentTypeOptions := createOptionBlockObjectsForTeams(teams) + incidentTypeText := slack.NewTextBlockObject(slack.PlainTextType, "Incident Type", false, false) + incidentTypePlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "The incident type for the incident to create", false, false) + incidentTypeOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, incidentTypePlaceholder, "type", incidentTypeOptions...) + incidentTypeBlock := slack.NewInputBlock("incident_type", incidentTypeText, nil, incidentTypeOption) + + severityOptions := createOptionBlockObjectsForSeverity(severities) + severityText := slack.NewTextBlockObject(slack.PlainTextType, "Severity", false, false) + severityTextPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "The severity for the incident to create", false, false) + severityOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, severityTextPlaceholder, "severity", severityOptions...) + severityBlock := slack.NewInputBlock("incident_severity", severityText, nil, severityOption) + + //pagerDutyImpactedServiceText := slack.NewTextBlockObject(slack.PlainTextType, "Pagerduty", false, false) + //pagerDutyImpactedServicePlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Select pagerduty impacted services", false, false) + //pagerDutyImpactedServiceElement := slack.NewPlainTextInputBlockElement(pagerDutyImpactedServicePlaceholder, "pagerduty_impacted") + //pagerDutyImpactedService := slack.NewInputBlock("Pagerduty impacted service", pagerDutyImpactedServiceText, nil, pagerDutyImpactedServiceElement) + //pagerDutyImpactedService.Optional = true + + incidentTitleText := slack.NewTextBlockObject(slack.PlainTextType, "Incident Title", false, false) + incidentTitlePlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Write something", false, false) + incidentTitleElement := slack.NewPlainTextInputBlockElement(incidentTitlePlaceholder, "title") + incidentTitle := slack.NewInputBlock("Incident title", incidentTitleText, nil, incidentTitleElement) + + incidentDescriptionText := slack.NewTextBlockObject(slack.PlainTextType, "Incident description", false, false) + incidentDescriptionPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Write something", false, false) + incidentDescriptionElement := slack.NewPlainTextInputBlockElement(incidentDescriptionPlaceholder, "description") + incidentDescriptionElement.Multiline = true + incidentDescription := slack.NewInputBlock("Incident description", incidentDescriptionText, nil, incidentDescriptionElement) + incidentDescription.Optional = true + + blocks := slack.Blocks{ + BlockSet: []slack.Block{ + headerSection, + incidentTypeBlock, + severityBlock, + //pagerDutyImpactedService, + incidentTitle, + incidentDescription, + }, + } + + return slack.ModalViewRequest{ + Type: slack.VTModal, + Title: titleText, + Close: closeText, + Submit: submitText, + Blocks: blocks, + PrivateMetadata: channel, + CallbackID: string(util.StartIncidentSubmit), + } +} + +func createOptionBlockObjectsForSeverity(options []severity.SeverityEntity) []*slack.OptionBlockObject { + optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) + for _, o := range options { + localObj := o + optionText := slack.NewTextBlockObject(slack.PlainTextType, localObj.Name, false, false) + optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(strconv.Itoa(int(localObj.ID)), optionText, nil)) + } + return optionBlockObjects +} + +func createOptionBlockObjectsForTeams(options []team.TeamEntity) []*slack.OptionBlockObject { + optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) + for _, o := range options { + localObj := o + optionText := slack.NewTextBlockObject(slack.PlainTextType, localObj.Name, false, false) + optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(strconv.Itoa(int(localObj.ID)), optionText, nil)) + } + return optionBlockObjects +} diff --git a/pkg/slack/houston/design/incident_assign.go b/internal/processor/action/view/incident_assign.go similarity index 61% rename from pkg/slack/houston/design/incident_assign.go rename to internal/processor/action/view/incident_assign.go index 7ad3d7e..2aaa72c 100644 --- a/pkg/slack/houston/design/incident_assign.go +++ b/internal/processor/action/view/incident_assign.go @@ -1,49 +1,48 @@ -package houston +package view import ( - "houston/entity" - "github.com/slack-go/slack" + "houston/common/util" + "houston/pkg/postgres/service/incident" ) func GenerateModalForIncidentAssign(channel slack.Channel) slack.ModalViewRequest { - - titleText := slack.NewTextBlockObject("plain_text", "Houston", false, false) - closeText := slack.NewTextBlockObject("plain_text", "Close", false, false) - submitText := slack.NewTextBlockObject("plain_text", "Submit", false, false) + titleText := slack.NewTextBlockObject(slack.PlainTextType, "Houston", false, false) + closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) + submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false) headerText := slack.NewTextBlockObject("mrkdwn", ":information_source: Incident roles are customizable by your organization. While a user can be assigned multiple roles, a role cannot be assigned to multiple users.", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) optionBlockObjects := []*slack.OptionBlockObject{ { Text: &slack.TextBlockObject{ - Type: "plain_text", - Text: string(entity.RESPONDER), + Type: slack.PlainTextType, + Text: string(incident.Responder), }, - Value: string(entity.RESPONDER), + Value: string(incident.Responder), }, { Text: &slack.TextBlockObject{ - Type: "plain_text", - Text: string(entity.SERVICE_OWNER), + Type: slack.PlainTextType, + Text: string(incident.ServiceOwner), }, - Value: string(entity.SERVICE_OWNER), + Value: string(incident.ServiceOwner), }, { Text: &slack.TextBlockObject{ - Type: "plain_text", - Text: string(entity.RETROSPECTIVE), + Type: slack.PlainTextType, + Text: string(incident.Retrospective), }, - Value: string(entity.RETROSPECTIVE), + Value: string(incident.Retrospective), }, } - rolePlaceholder := slack.NewTextBlockObject("plain_text", "Select a role", false, false) + rolePlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Select a role", false, false) roleTypeOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, rolePlaceholder, "role_type", optionBlockObjects...) roleTypeText := slack.NewTextBlockObject(slack.PlainTextType, "Roles", false, false) roleBlock := slack.NewInputBlock("role_type", roleTypeText, nil, roleTypeOption) - userPlaceholder := slack.NewTextBlockObject("plain_text", "Select people", false, false) + userPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Select people", false, false) userTypeOption := slack.NewOptionsSelectBlockElement(slack.OptTypeUser, userPlaceholder, "users_select") userTypeText := slack.NewTextBlockObject(slack.PlainTextType, "Users", false, false) userBlock := slack.NewInputBlock("user_type", userTypeText, nil, userTypeOption) @@ -55,13 +54,13 @@ func GenerateModalForIncidentAssign(channel slack.Channel) slack.ModalViewReques }, } return slack.ModalViewRequest{ - Type: slack.ViewType("modal"), + Type: slack.VTModal, Title: titleText, Close: closeText, Submit: submitText, Blocks: blocks, PrivateMetadata: channel.ID, - CallbackID: "assignIncidentRole", + CallbackID: util.AssignIncidentRoleSubmit, } } diff --git a/pkg/slack/houston/design/incident_description.go b/internal/processor/action/view/incident_description.go similarity index 55% rename from pkg/slack/houston/design/incident_description.go rename to internal/processor/action/view/incident_description.go index abc1eac..8748ef6 100644 --- a/pkg/slack/houston/design/incident_description.go +++ b/internal/processor/action/view/incident_description.go @@ -1,20 +1,20 @@ -package houston +package view import ( "github.com/slack-go/slack" - "gorm.io/gorm" + "houston/common/util" ) -func BuildIncidentUpdateDescriptionModal(db *gorm.DB, channel slack.Channel, description string) slack.ModalViewRequest { - titleText := slack.NewTextBlockObject("plain_text", "Set Description", false, false) - closeText := slack.NewTextBlockObject("plain_text", "Close", false, false) - submitText := slack.NewTextBlockObject("plain_text", "Submit", false, false) +func BuildIncidentUpdateDescriptionModal(channel slack.Channel, 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) headerText := slack.NewTextBlockObject("mrkdwn", "Description", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) - incidentDescriptionText := slack.NewTextBlockObject("plain_text", "Incident description", false, false) - incidentDescriptionPlaceholder := slack.NewTextBlockObject("plain_text", "Write something", false, false) + incidentDescriptionText := slack.NewTextBlockObject(slack.PlainTextType, "Incident description", false, false) + incidentDescriptionPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Write something", false, false) incidentDescriptionElement := slack.NewPlainTextInputBlockElement(incidentDescriptionPlaceholder, "incident_description") incidentDescriptionElement.Multiline = true incidentDescriptionElement.InitialValue = description @@ -30,13 +30,13 @@ func BuildIncidentUpdateDescriptionModal(db *gorm.DB, channel slack.Channel, des } return slack.ModalViewRequest{ - Type: slack.ViewType("modal"), + Type: slack.VTModal, Title: titleText, Close: closeText, Submit: submitText, Blocks: blocks, PrivateMetadata: channel.ID, - CallbackID: "setIncidentDescription", + CallbackID: util.SetIncidentDescriptionSubmit, } } diff --git a/pkg/slack/houston/design/blazeless_section.go b/internal/processor/action/view/incident_section.go similarity index 91% rename from pkg/slack/houston/design/blazeless_section.go rename to internal/processor/action/view/incident_section.go index b38666a..234c2e4 100644 --- a/pkg/slack/houston/design/blazeless_section.go +++ b/internal/processor/action/view/incident_section.go @@ -1,13 +1,16 @@ -package houston +package view -import "github.com/slack-go/slack" +import ( + "github.com/slack-go/slack" + "houston/common/util" +) -func ButtonDesign() map[string]interface{} { +func NewIncidentBlock() map[string]interface{} { payload := map[string]interface{}{ "blocks": []slack.Block{ slack.NewActionBlock("start_incident_button", slack.NewButtonBlockElement( - "start_incident_button", + string(util.StartIncident), "start_incident_button_value", &slack.TextBlockObject{ Type: slack.PlainTextType, @@ -15,7 +18,7 @@ func ButtonDesign() map[string]interface{} { }, ), slack.NewButtonBlockElement( - "show_incidents_button", + string(util.ShowIncidents), "show_incidents_button_value", &slack.TextBlockObject{ Type: slack.PlainTextType, @@ -42,11 +45,9 @@ func ButtonDesign() map[string]interface{} { } return payload - - // client.Ack(*request, payload) } -func OptionsBlock() map[string]interface{} { +func ExistingIncidentOptionsBlock() map[string]interface{} { return map[string]interface{}{ "blocks": []slack.Block{ incidentSectionBlock(), @@ -70,49 +71,49 @@ func incidentSectionBlock() *slack.SectionBlock { Type: "plain_text", Text: "Assign Incident Role", }, - Value: "assignIncidentRole", + Value: util.AssignIncidentRole, }, { Text: &slack.TextBlockObject{ Type: "plain_text", Text: "Resolve Incident", }, - Value: "resolveIncident", + Value: util.ResolveIncident, }, { Text: &slack.TextBlockObject{ Type: "plain_text", Text: "Set Incident Status", }, - Value: "setIncidentStatus", + Value: util.SetIncidentStatus, }, { Text: &slack.TextBlockObject{ Type: "plain_text", Text: "Set Incident Type", }, - Value: "setIncidentType", + Value: util.SetIncidentType, }, { Text: &slack.TextBlockObject{ Type: "plain_text", Text: "Set Incident Severity", }, - Value: "setIncidentSeverity", + Value: util.SetIncidentSeverity, }, { Text: &slack.TextBlockObject{ Type: "plain_text", Text: "Set Incident Title", }, - Value: "setIncidentTitle", + Value: util.SetIncidentTitle, }, { Text: &slack.TextBlockObject{ Type: "plain_text", Text: "Set Incident Description", }, - Value: "setIncidentDescription", + Value: util.SetIncidentDescription, }, } @@ -187,21 +188,21 @@ func tagsSectionBlock() *slack.SectionBlock { Type: "plain_text", Text: "Add Tag(s)", }, - Value: "addTags", + Value: util.AddTags, }, { Text: &slack.TextBlockObject{ Type: "plain_text", Text: "Show Tags", }, - Value: "showTags", + Value: util.ShowTags, }, { Text: &slack.TextBlockObject{ Type: "plain_text", Text: "Remove Tag", }, - Value: "removeTag", + Value: util.RemoveTag, }, } diff --git a/pkg/slack/houston/design/incident_severity.go b/internal/processor/action/view/incident_severity.go similarity index 70% rename from pkg/slack/houston/design/incident_severity.go rename to internal/processor/action/view/incident_severity.go index 96c08a8..a6f87a0 100644 --- a/pkg/slack/houston/design/incident_severity.go +++ b/internal/processor/action/view/incident_severity.go @@ -1,17 +1,18 @@ -package houston +package view import ( "fmt" - "houston/entity" + "houston/common/util" + "houston/pkg/postgres/service/severity" "strconv" "github.com/slack-go/slack" ) -func BuildIncidentUpdateSeverityModal(channel slack.Channel, incidentSeverity []entity.SeverityEntity) slack.ModalViewRequest { - titleText := slack.NewTextBlockObject("plain_text", "Set Severity of Incident", false, false) - closeText := slack.NewTextBlockObject("plain_text", "Close", false, false) - submitText := slack.NewTextBlockObject("plain_text", "Submit", false, false) +func BuildIncidentUpdateSeverityModal(channel slack.Channel, 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) headerText := slack.NewTextBlockObject("mrkdwn", "Severity", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) @@ -30,18 +31,18 @@ func BuildIncidentUpdateSeverityModal(channel slack.Channel, incidentSeverity [] } return slack.ModalViewRequest{ - Type: slack.ViewType("modal"), + Type: slack.VTModal, Title: titleText, Close: closeText, Submit: submitText, Blocks: blocks, PrivateMetadata: channel.ID, - CallbackID: "setIncidentSeverity", + CallbackID: util.SetIncidentSeveritySubmit, } } -func createIncidentSeverityBlock(options []entity.SeverityEntity) []*slack.OptionBlockObject { +func createIncidentSeverityBlock(options []severity.SeverityEntity) []*slack.OptionBlockObject { optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) for _, o := range options { txt := fmt.Sprintf("%s (%s)", o.Name, o.Description) diff --git a/pkg/slack/houston/design/blazeless_incident_status_update_modal.go b/internal/processor/action/view/incident_status_update_modal.go similarity index 63% rename from pkg/slack/houston/design/blazeless_incident_status_update_modal.go rename to internal/processor/action/view/incident_status_update_modal.go index 7d1da03..908bcd0 100644 --- a/pkg/slack/houston/design/blazeless_incident_status_update_modal.go +++ b/internal/processor/action/view/incident_status_update_modal.go @@ -1,26 +1,23 @@ -package houston +package view import ( "fmt" - "houston/entity" - "houston/pkg/postgres/query" + "houston/common/util" + "houston/pkg/postgres/service/incident" "strconv" "github.com/slack-go/slack" - "gorm.io/gorm" ) -func BuildIncidentUpdateStatusModal(db *gorm.DB, channel slack.Channel) slack.ModalViewRequest { - titleText := slack.NewTextBlockObject("plain_text", "Set Status of Incident", false, false) - closeText := slack.NewTextBlockObject("plain_text", "Close", false, false) - submitText := slack.NewTextBlockObject("plain_text", "Submit", false, false) +func BuildIncidentUpdateStatusModal(statuses []incident.IncidentStatusEntity, channel slack.Channel) 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) headerText := slack.NewTextBlockObject("mrkdwn", "Status", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) - incidentStatus, _ := query.FetchAllIncidentStatus(db) - - incidentStatusBlockOption := createIncidentStatusBlock(incidentStatus) + incidentStatusBlockOption := createIncidentStatusBlock(statuses) incidentStatusText := slack.NewTextBlockObject(slack.PlainTextType, "Incident Status", false, false) incidentStatusOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, nil, "incident_status_modal_request", incidentStatusBlockOption...) @@ -34,18 +31,18 @@ func BuildIncidentUpdateStatusModal(db *gorm.DB, channel slack.Channel) slack.Mo } return slack.ModalViewRequest{ - Type: slack.ViewType("modal"), + Type: slack.VTModal, Title: titleText, Close: closeText, Submit: submitText, Blocks: blocks, PrivateMetadata: channel.ID, - CallbackID: "setIncidentStatus", + CallbackID: util.SetIncidentStatusSubmit, } } -func createIncidentStatusBlock(options []entity.IncidentStatusEntity) []*slack.OptionBlockObject { +func createIncidentStatusBlock(options []incident.IncidentStatusEntity) []*slack.OptionBlockObject { optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) for _, o := range options { txt := fmt.Sprintf("%s - %s", o.Name, o.Description) diff --git a/internal/processor/action/view/incident_summary_section.go b/internal/processor/action/view/incident_summary_section.go new file mode 100644 index 0000000..da22647 --- /dev/null +++ b/internal/processor/action/view/incident_summary_section.go @@ -0,0 +1,40 @@ +package view + +import ( + "fmt" + "github.com/slack-go/slack" + "houston/common/util" + "houston/pkg/postgres/service/incident" + "houston/pkg/postgres/service/severity" + "houston/pkg/postgres/service/team" +) + +func IncidentSummarySection(incident *incident.IncidentEntity, team *team.TeamEntity, severity *severity.SeverityEntity, incidentStatus *incident.IncidentStatusEntity) slack.Blocks { + return slack.Blocks{ + BlockSet: []slack.Block{ + buildTypeAndChannelSectionBlock(incident, team.Name, severity.Name, incidentStatus.Name)}, + } +} + +func buildTypeAndChannelSectionBlock(incident *incident.IncidentEntity, teamName, severityName, incidentStatus string) *slack.SectionBlock { + fields := []*slack.TextBlockObject{ + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*<@%s>* \n*%s* - *%s*\n", incident.CreatedBy, incident.IncidentName, incident.Title), false, false), + slack.NewTextBlockObject("mrkdwn", "\n", false, false), + slack.NewTextBlockObject("mrkdwn", incident.Description, false, false), + slack.NewTextBlockObject("mrkdwn", "\n", false, false), + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Type*\n%s", teamName), false, false), + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Channel*\n<#%s>", incident.SlackChannel), false, false), + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Severity*\n%s", severityName), false, false), + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Ticket*\n%s", "Integration Disabled"), false, false), + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Status*\n%s", incidentStatus), false, false), + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Meeting*\n%s", "Integration Disabled"), false, false), + } + block := slack.NewSectionBlock(nil, fields, nil) + + return block +} + +func IncidentEphemeralMessage(incident *incident.IncidentEntity, blocks slack.Blocks) slack.MsgOption { + color := util.GetColorBySeverity(incident.SeverityId) + return slack.MsgOptionAttachments(slack.Attachment{Blocks: blocks, Color: color}) +} diff --git a/pkg/slack/houston/design/incident_title.go b/internal/processor/action/view/incident_title.go similarity index 54% rename from pkg/slack/houston/design/incident_title.go rename to internal/processor/action/view/incident_title.go index bddd7c7..512faad 100644 --- a/pkg/slack/houston/design/incident_title.go +++ b/internal/processor/action/view/incident_title.go @@ -1,20 +1,20 @@ -package houston +package view import ( "github.com/slack-go/slack" - "gorm.io/gorm" + "houston/common/util" ) -func BuildIncidentUpdateTitleModal(db *gorm.DB, channel slack.Channel, title string) slack.ModalViewRequest { - titleText := slack.NewTextBlockObject("plain_text", "Set Title of Incident", false, false) - closeText := slack.NewTextBlockObject("plain_text", "Close", false, false) - submitText := slack.NewTextBlockObject("plain_text", "Submit", false, false) +func BuildIncidentUpdateTitleModal(channel slack.Channel, title string) slack.ModalViewRequest { + titleText := slack.NewTextBlockObject(slack.PlainTextType, "Set Title of Incident", false, false) + closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) + submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false) headerText := slack.NewTextBlockObject("mrkdwn", "Title", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) - incidentTitleText := slack.NewTextBlockObject("plain_text", "Incident Title", false, false) - incidentTitlePlaceholder := slack.NewTextBlockObject("plain_text", "Write something", false, false) + incidentTitleText := slack.NewTextBlockObject(slack.PlainTextType, "Incident Title", false, false) + incidentTitlePlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Write something", false, false) incidentTitleElement := slack.NewPlainTextInputBlockElement(incidentTitlePlaceholder, "incident_title") incidentTitleElement.InitialValue = title incidentBelowText := slack.NewTextBlockObject("plain_text", "The title to set for the incident.", false, false) @@ -28,13 +28,13 @@ func BuildIncidentUpdateTitleModal(db *gorm.DB, channel slack.Channel, title str } return slack.ModalViewRequest{ - Type: slack.ViewType("modal"), + Type: slack.VTModal, Title: titleText, Close: closeText, Submit: submitText, Blocks: blocks, PrivateMetadata: channel.ID, - CallbackID: "setIncidentTitle", + CallbackID: util.SetIncidentTitleSubmit, } } diff --git a/internal/processor/action/view/incident_update_tags.go b/internal/processor/action/view/incident_update_tags.go new file mode 100644 index 0000000..aead0ec --- /dev/null +++ b/internal/processor/action/view/incident_update_tags.go @@ -0,0 +1,107 @@ +package view + +import ( + "fmt" + "github.com/slack-go/slack" + "houston/common/util" + "houston/pkg/postgres/service/tag" + "strconv" +) + +func BuildIncidentUpdateTagModal(channel slack.Channel, inputBlocks []slack.InputBlock) slack.ModalViewRequest { + titleText := slack.NewTextBlockObject(slack.PlainTextType, "Edit Tags", false, false) + closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) + submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false) + + var localBlocks []slack.Block + for _, block := range inputBlocks { + localBlocks = append(localBlocks, block) + } + + blocks := slack.Blocks{ + BlockSet: localBlocks, + } + + return slack.ModalViewRequest{ + Type: slack.VTModal, + Title: titleText, + Close: closeText, + Submit: submitText, + Blocks: blocks, + PrivateMetadata: channel.ID, + CallbackID: util.UpdateTagSubmit, + } + +} + +func CreateInputBlock(tagEntity tag.TagDTO, tagValues []tag.TagValueEntity, initialTagValues interface{}, isOptional bool) *slack.InputBlock { + text := slack.NewTextBlockObject(slack.PlainTextType, tagEntity.Label, false, false) + var element slack.BlockElement + + switch tagEntity.Type { + case tag.FreeText: + { + element = createPlainTextInputBlockElement(tagEntity, initialTagValues) + } + case tag.SingleValue: + { + element = createOptionsSelectBlockElement(tagEntity, tagValues, initialTagValues.([]tag.TagValueEntity)) + } + case tag.MultiValue: + { + element = createMultiOptionsSelectBlockElement(tagEntity, tagValues, initialTagValues.([]tag.TagValueEntity)) + } + default: + { + return nil + } + } + + block := slack.NewInputBlock(tagEntity.Name, text, nil, element) + block.Optional = isOptional + + return block +} + +func createPlainTextInputBlockElement(tagEntity tag.TagDTO, value interface{}) *slack.PlainTextInputBlockElement { + placeholder := slack.NewTextBlockObject(slack.PlainTextType, tagEntity.PlaceHolder, false, false) + element := slack.NewPlainTextInputBlockElement(placeholder, tagEntity.ActionId) + + if value != nil { + element.InitialValue = fmt.Sprintf("%v", value) + } else { + element.InitialValue = "" + } + + return element +} + +func createOptionsSelectBlockElement(tagEntity tag.TagDTO, tagValues []tag.TagValueEntity, initialTagValues []tag.TagValueEntity) *slack.SelectBlockElement { + blockOptions := createTagOptions(tagValues) + placeholder := slack.NewTextBlockObject(slack.PlainTextType, tagEntity.PlaceHolder, false, false) + element := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, placeholder, tagEntity.ActionId, blockOptions...) + if initialTagValues != nil { + element.InitialOption = createTagOptions(initialTagValues)[0] + } + return element +} + +func createMultiOptionsSelectBlockElement(tagEntity tag.TagDTO, tagValues []tag.TagValueEntity, initialTagValues []tag.TagValueEntity) *slack.MultiSelectBlockElement { + blockOptions := createTagOptions(tagValues) + placeholder := slack.NewTextBlockObject(slack.PlainTextType, tagEntity.PlaceHolder, false, false) + element := slack.NewOptionsMultiSelectBlockElement(slack.MultiOptTypeStatic, placeholder, tagEntity.ActionId, blockOptions...) + if initialTagValues != nil { + element.InitialOptions = createTagOptions(initialTagValues) + } + return element +} + +func createTagOptions(tagValues []tag.TagValueEntity) []*slack.OptionBlockObject { + optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(tagValues)) + for _, o := range tagValues { + txt := fmt.Sprintf("%s", o.Value) + optionText := slack.NewTextBlockObject(slack.PlainTextType, txt, false, false) + optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(strconv.FormatUint(uint64(o.ID), 10), optionText, nil)) + } + return optionBlockObjects +} diff --git a/pkg/slack/houston/design/incident_update_type.go b/internal/processor/action/view/incident_update_type.go similarity index 71% rename from pkg/slack/houston/design/incident_update_type.go rename to internal/processor/action/view/incident_update_type.go index 96a4815..e1e39b1 100644 --- a/pkg/slack/houston/design/incident_update_type.go +++ b/internal/processor/action/view/incident_update_type.go @@ -1,17 +1,18 @@ -package houston +package view import ( "fmt" - "houston/entity" + "houston/common/util" + "houston/pkg/postgres/service/team" "strconv" "github.com/slack-go/slack" ) -func BuildIncidentUpdateTypeModal(channel slack.Channel, teams []entity.TeamEntity) slack.ModalViewRequest { - titleText := slack.NewTextBlockObject("plain_text", "Set Type of Incident", false, false) - closeText := slack.NewTextBlockObject("plain_text", "Close", false, false) - submitText := slack.NewTextBlockObject("plain_text", "Submit", false, false) +func BuildIncidentUpdateTypeModal(channel slack.Channel, 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) headerText := slack.NewTextBlockObject("mrkdwn", "Type", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) @@ -30,18 +31,18 @@ func BuildIncidentUpdateTypeModal(channel slack.Channel, teams []entity.TeamEnti } return slack.ModalViewRequest{ - Type: slack.ViewType("modal"), + Type: slack.VTModal, Title: titleText, Close: closeText, Submit: submitText, Blocks: blocks, PrivateMetadata: channel.ID, - CallbackID: "setIncidentType", + CallbackID: util.SetIncidentTypeSubmit, } } -func createIncidentTypeBlock(options []entity.TeamEntity) []*slack.OptionBlockObject { +func createIncidentTypeBlock(options []team.TeamEntity) []*slack.OptionBlockObject { optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) for _, o := range options { txt := fmt.Sprintf("%s", o.Name) diff --git a/internal/processor/action/view/show_incidents_button_section.go b/internal/processor/action/view/show_incidents_button_section.go new file mode 100644 index 0000000..bb216de --- /dev/null +++ b/internal/processor/action/view/show_incidents_button_section.go @@ -0,0 +1,37 @@ +package view + +import ( + "houston/pkg/postgres/service/incident" + + "github.com/slack-go/slack" +) + +func GenerateModalForShowIncidentsButtonSection(incident []incident.IncidentSeverityTeamDTO) []slack.Block { + contextBlock := slack.NewContextBlock("", slack.NewTextBlockObject("mrkdwn", ":eye: Only visible to you", false, false)) + + var sectionBlocks []*slack.SectionBlock + + if incident != nil { + for i := 0; i < len(incident); i++ { + fields := []*slack.TextBlockObject{ + slack.NewTextBlockObject("mrkdwn", "\n`"+incident[i].SeverityName+"` `"+incident[i].TeamName+" Incident` \n "+incident[i].Title+"\n <#"+incident[i].SlackChannel+">", false, false), + } + sectionBlocks = append(sectionBlocks, slack.NewSectionBlock(nil, fields, nil)) + } + } else { + fields := []*slack.TextBlockObject{ + slack.NewTextBlockObject("mrkdwn", "\n`No Open Incidents`\n", false, false), + } + sectionBlocks = append(sectionBlocks, slack.NewSectionBlock(nil, fields, nil)) + } + + blocks := []slack.Block{ + contextBlock, + } + for i := 0; i < len(sectionBlocks); i++ { + blocks = append(blocks, sectionBlocks[i]) + } + + return blocks + +} diff --git a/internal/processor/event_type_interactive_processor.go b/internal/processor/event_type_interactive_processor.go new file mode 100644 index 0000000..7242590 --- /dev/null +++ b/internal/processor/event_type_interactive_processor.go @@ -0,0 +1,236 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/common/util" + "houston/internal/processor/action" + "houston/pkg/postgres/service/incident" + "houston/pkg/postgres/service/severity" + "houston/pkg/postgres/service/tag" + "houston/pkg/postgres/service/team" + "houston/pkg/slackbot" +) + +type interactiveEventProcessor interface { + ProcessCommand(callback slack.InteractionCallback, request *socketmode.Request) +} + +type BlockActionProcessor struct { + logger *zap.Logger + socketModeClient *socketmode.Client + startIncidentBlockAction *action.StartIncidentBlockAction + showIncidentsAction *action.ShowIncidentsAction + assignIncidentAction *action.AssignIncidentAction + incidentResolveAction *action.ResolveIncidentAction + incidentUpdateAction *action.UpdateIncidentAction + incidentUpdateTypeAction *action.IncidentUpdateTypeAction + incidentUpdateSeverityAction *action.IncidentUpdateSevertityAction + incidentUpdateTitleAction *action.IncidentUpdateTitleAction + incidentUpdateDescriptionAction *action.IncidentUpdateDescriptionAction + incidentUpdateTagsAction *action.IncidentUpdateTagsAction + incidentShowTagsAction *action.IncidentShowTagsAction +} + +func NewBlockActionProcessor(logger *zap.Logger, socketModeClient *socketmode.Client, incidentService *incident.Service, + teamService *team.Service, severityService *severity.Service, tagService *tag.Service, + slackbotClient *slackbot.Client) *BlockActionProcessor { + return &BlockActionProcessor{ + logger: logger, + socketModeClient: socketModeClient, + startIncidentBlockAction: action.NewStartIncidentBlockAction(socketModeClient, logger, teamService, severityService), + showIncidentsAction: action.ShowIncidentsProcessor(socketModeClient, logger, incidentService), + assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, logger, incidentService), + incidentResolveAction: action.NewIncidentResolveProcessor(socketModeClient, logger, incidentService), + incidentUpdateAction: action.NewIncidentUpdateAction(socketModeClient, logger, incidentService), + incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, logger, incidentService, teamService, slackbotClient), + incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, logger, incidentService, severityService, slackbotClient), + incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, logger, incidentService), + incidentUpdateDescriptionAction: action.NewIncidentUpdateDescriptionAction(socketModeClient, logger, incidentService), + incidentUpdateTagsAction: action.NewIncidentUpdateTagsAction(socketModeClient, logger, incidentService, teamService, tagService), + incidentShowTagsAction: action.NewIncidentShowTagsProcessor(socketModeClient, logger, incidentService, tagService), + } +} + +func (bap *BlockActionProcessor) ProcessCommand(callback slack.InteractionCallback, request *socketmode.Request) { + defer func() { + if r := recover(); r != nil { + bap.logger.Error(fmt.Sprintf("[BAP] Exception occurred: %v", r.(error))) + } + }() + + actionId := util.BlockActionType(callback.ActionCallback.BlockActions[0].ActionID) + bap.logger.Info("process button callback event", zap.Any("action_id", actionId), + zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), + zap.String("user_name", callback.User.Name)) + + switch actionId { + case util.StartIncident: + { + bap.startIncidentBlockAction.ProcessAction(request, callback) + } + case util.ShowIncidents: + { + bap.showIncidentsAction.ProcessAction(callback.Channel, callback.User, callback.TriggerID, request) + } + case util.Incident: + { + bap.processIncidentCommands(callback, request) + } + + case util.Tags: + { + bap.processTagsCommands(callback, request) + } + default: + { + msgOption := slack.MsgOptionText(fmt.Sprintf("We are working on it"), false) + _, err := bap.socketModeClient.PostEphemeral(callback.Channel.ID, callback.User.ID, msgOption) + if err != nil { + bap.logger.Error("[BAP] houston slackbot PostEphemeral command failed for Working On It features", + zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), + zap.String("user_id", callback.User.ID), zap.Error(err)) + return + } + var payload interface{} + bap.socketModeClient.Ack(*request, payload) + } + } +} + +func (bap *BlockActionProcessor) processIncidentCommands(callback slack.InteractionCallback, request *socketmode.Request) { + action1 := util.BlockActionType(callback.ActionCallback.BlockActions[0].SelectedOption.Value) + switch action1 { + case util.AssignIncidentRole: + { + bap.assignIncidentAction.IncidentAssignProcess(callback, request) + } + case util.ResolveIncident: + { + bap.incidentResolveAction.IncidentResolveProcess(callback, request) + } + case util.SetIncidentStatus: + { + bap.incidentUpdateAction.IncidentUpdateStatusRequestProcess(callback, request) + } + case util.SetIncidentType: + { + bap.incidentUpdateTypeAction.IncidentUpdateTypeRequestProcess(callback, request) + } + case util.SetIncidentSeverity: + { + bap.incidentUpdateSeverityAction.IncidentUpdateSeverityRequestProcess(callback, request) + } + case util.SetIncidentTitle: + { + bap.incidentUpdateTitleAction.IncidentUpdateTitleRequestProcess(callback, request) + } + case util.SetIncidentDescription: + { + bap.incidentUpdateDescriptionAction.IncidentUpdateDescriptionRequestProcess(callback, request) + } + } +} + +func (bap *BlockActionProcessor) processTagsCommands(callback slack.InteractionCallback, request *socketmode.Request) { + action1 := util.BlockActionType(callback.ActionCallback.BlockActions[0].SelectedOption.Value) + switch action1 { + case util.AddTags: + { + bap.incidentUpdateTagsAction.IncidentUpdateTagsRequestProcess(callback, request) + } + case util.ShowTags: + { + bap.incidentShowTagsAction.IncidentShowTagsRequestProcess(callback, request) + } + case util.RemoveTag: + { + bap.incidentUpdateTagsAction.IncidentUpdateTagsRequestProcess(callback, request) + } + } +} + +type ViewSubmissionProcessor struct { + logger *zap.Logger + socketModeClient *socketmode.Client + incidentChannelMessageUpdateAction *action.IncidentChannelMessageUpdateAction + createIncidentAction *action.CreateIncidentAction + assignIncidentAction *action.AssignIncidentAction + updateIncidentAction *action.UpdateIncidentAction + incidentUpdateTitleAction *action.IncidentUpdateTitleAction + incidentUpdateDescriptionAction *action.IncidentUpdateDescriptionAction + incidentUpdateSeverityAction *action.IncidentUpdateSevertityAction + incidentUpdateTypeAction *action.IncidentUpdateTypeAction + incidentUpdateTagsAction *action.IncidentUpdateTagsAction +} + +func NewViewSubmissionProcessor(logger *zap.Logger, socketModeClient *socketmode.Client, incidentService *incident.Service, + teamService *team.Service, severityService *severity.Service, tagService *tag.Service, + slackbotClient *slackbot.Client) *ViewSubmissionProcessor { + return &ViewSubmissionProcessor{ + logger: logger, + socketModeClient: socketModeClient, + incidentChannelMessageUpdateAction: action.NewIncidentChannelMessageUpdateAction(socketModeClient, logger, incidentService, teamService, severityService), + createIncidentAction: action.NewCreateIncidentProcessor(socketModeClient, logger, incidentService, teamService, severityService, slackbotClient), + assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, logger, incidentService), + updateIncidentAction: action.NewIncidentUpdateAction(socketModeClient, logger, incidentService), + incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, logger, incidentService), + incidentUpdateDescriptionAction: action.NewIncidentUpdateDescriptionAction(socketModeClient, logger, incidentService), + incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, logger, incidentService, severityService, slackbotClient), + incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, logger, incidentService, teamService, slackbotClient), + incidentUpdateTagsAction: action.NewIncidentUpdateTagsAction(socketModeClient, logger, incidentService, teamService, tagService), + } +} + +func (vsp *ViewSubmissionProcessor) ProcessCommand(callback slack.InteractionCallback, request *socketmode.Request) { + defer func() { + if r := recover(); r != nil { + vsp.logger.Error(fmt.Sprintf("[VSP] Exception occurred: %v", r.(error))) + } + }() + + var callbackId = util.ViewSubmissionType(callback.View.CallbackID) + switch callbackId { + case util.StartIncidentSubmit: + { + vsp.createIncidentAction.CreateIncidentModalCommandProcessing(callback, request) + } + case util.AssignIncidentRoleSubmit: + { + vsp.assignIncidentAction.IncidentAssignModalCommandProcessing(callback, request) + } + case util.SetIncidentStatusSubmit: + { + vsp.updateIncidentAction.IncidentUpdateStatus(callback, request, callback.Channel, callback.User) + } + case util.SetIncidentTitleSubmit: + { + vsp.incidentUpdateTitleAction.IncidentUpdateTitle(callback, request, callback.Channel, callback.User) + } + case util.SetIncidentDescriptionSubmit: + { + vsp.incidentUpdateDescriptionAction.IncidentUpdateDescription(callback, request, callback.Channel, callback.User) + } + case util.SetIncidentSeveritySubmit: + { + vsp.incidentUpdateSeverityAction.IncidentUpdateSeverity(callback, request, callback.Channel, callback.User) + } + case util.SetIncidentTypeSubmit: + { + vsp.incidentUpdateTypeAction.IncidentUpdateType(callback, request, callback.Channel, callback.User) + } + case util.UpdateTagSubmit: + { + vsp.incidentUpdateTagsAction.IncidentUpdateTags(callback, request) + } + default: + { + return + } + } + + // updates the incident info in all the channels where queried + vsp.incidentChannelMessageUpdateAction.ProcessAction(callback.View.PrivateMetadata) +} diff --git a/internal/processor/events_api_event_processor.go b/internal/processor/events_api_event_processor.go new file mode 100644 index 0000000..10d90cd --- /dev/null +++ b/internal/processor/events_api_event_processor.go @@ -0,0 +1,43 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/slackevents" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/internal/processor/action" + "houston/pkg/postgres/service/incident" + "houston/pkg/postgres/service/severity" + "houston/pkg/postgres/service/team" +) + +type eventsApiEventProcessor interface { + ProcessCommand(event *slackevents.EventsAPIEvent, request *socketmode.Request) +} + +type MemberJoinedCallbackEventProcessor struct { + logger *zap.Logger + socketModeClient *socketmode.Client + memberJoinAction *action.MemberJoinAction +} + +func NewMemberJoinedCallbackEventProcessor(logger *zap.Logger, socketModeClient *socketmode.Client, + incidentService *incident.Service, teamService *team.Service, severityService *severity.Service) *MemberJoinedCallbackEventProcessor { + return &MemberJoinedCallbackEventProcessor{ + logger: logger, + socketModeClient: socketModeClient, + memberJoinAction: action.NewMemberJoinAction(socketModeClient, logger, incidentService, teamService, severityService), + } +} + +func (mjc *MemberJoinedCallbackEventProcessor) ProcessCommand(event *slackevents.MemberJoinedChannelEvent, request *socketmode.Request) { + defer func() { + if r := recover(); r != nil { + mjc.logger.Error(fmt.Sprintf("[MJC] Exception occurred: %v", r.(error))) + } + }() + + mjc.memberJoinAction.PerformAction(event) + var payload interface{} + mjc.socketModeClient.Ack(*request, payload) +} diff --git a/internal/processor/slash_command_processor.go b/internal/processor/slash_command_processor.go new file mode 100644 index 0000000..4700fab --- /dev/null +++ b/internal/processor/slash_command_processor.go @@ -0,0 +1,33 @@ +package processor + +import ( + "fmt" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/internal/processor/action" + "houston/pkg/postgres/service/incident" +) + +type SlashCommandProcessor struct { + logger *zap.Logger + socketModeClient *socketmode.Client + slashCommandAction *action.SlashCommandAction +} + +func NewSlashCommandProcessor(logger *zap.Logger, socketModeClient *socketmode.Client, incidentService *incident.Service) *SlashCommandProcessor { + return &SlashCommandProcessor{ + logger: logger, + socketModeClient: socketModeClient, + slashCommandAction: action.NewSlashCommandAction(incidentService, logger, socketModeClient), + } +} + +func (scp *SlashCommandProcessor) ProcessSlashCommand(event socketmode.Event) { + defer func() { + if r := recover(); r != nil { + scp.logger.Error(fmt.Sprintf("[SCP] Exception occurred: %v", r.(error))) + } + }() + + scp.slashCommandAction.PerformAction(&event) +} diff --git a/model/.DS_Store b/model/.DS_Store deleted file mode 100644 index 47ce1066641c01b6b3115aa70f5252e71712faf8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJ5Iwu5So4_RyS3&URlf;${ zqCg0t8ENO)yKlVD&z9FqM4>w$wuxFq6rnLTwvcLq$GKIaVLdy*AlDetjPAx4?cN}( zu)X6iI>66vlWu85H#A3HKEKISH(1k4ep?o^ z9rC1+=$X-!GP;JMk-kw24pXX}qNaz_gSV3x(NwwCBwMNaidWVpbFM?nn9gZHS2cuh$ZH#t?#=*Q? laan=`S&G5srFaib0{t-`029Mp5j_b15fB?ZxdXrIzz0omZQlR@ diff --git a/model/create_incident.go b/model/create_incident.go deleted file mode 100644 index 82d896a..0000000 --- a/model/create_incident.go +++ /dev/null @@ -1,30 +0,0 @@ -package model - -import ( - "houston/entity" - "time" -) - -type CreateIncident struct { - IncidentType string `json:"incident_type,omitempty"` - Pagerduty string `json:"pagerduty,omitempty"` - IncidentTitle string `json:"incident_title,omitempty"` - IncidentDescription string `json:"incident_description,omitempty"` - RequestOriginatedSlackChannel string - IncidentSeverity string `json:"incident_severity,omitempty"` - Status entity.IncidentStatus - IncidentName string - SlackChannel string - DetectionTime *time.Time - CustomerImpactStartTime time.Time - CustomerImpactEndTime *time.Time - TeamsId int - JiraId string - ConfluenceId string - SeverityTat time.Time - RemindMeAt *time.Time - EnableReminder bool - CreatedBy string - UpdatedBy string - Version int -} diff --git a/model/request/create_message.go b/model/request/create_message.go deleted file mode 100644 index aacebfe..0000000 --- a/model/request/create_message.go +++ /dev/null @@ -1,7 +0,0 @@ -package request - -type CreateMessage struct { - SlackChannel string `gorm:"column:slack_channel"` - IncidentName string `gorm:"column:incident_name"` - MessageTimeStamp string `gorm:"column:message_timestamp"` -} diff --git a/model/request/incident_role.go b/model/request/incident_role.go deleted file mode 100644 index 8203012..0000000 --- a/model/request/incident_role.go +++ /dev/null @@ -1,10 +0,0 @@ -package request - -import "houston/entity" - -type AddIncidentRoleRequest struct { - UserId string `json:"users_select,omitempty"` - Role entity.IncidentRole `json:"role_type,omitempty"` - IncidentId int - CreatedById string -} diff --git a/model/request/severity.go b/model/request/severity.go deleted file mode 100644 index 5065050..0000000 --- a/model/request/severity.go +++ /dev/null @@ -1,17 +0,0 @@ -package request - -type AddSeverityRequest struct { - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` -} - -type AddSeverityUserMappingRequest struct { - SeverityId uint16 `json:"severity_id,omitempty"` - Users []AddSeverityUserData `json:"users,omitempty"` -} - -type AddSeverityUserData struct { - SlackUserId string `json:"slack_user_id,omitempty"` - Primary bool `json:"primary,omitempty"` - Secondary bool `json:"secondary,omitempty"` -} diff --git a/pkg/postgres/config.go b/pkg/postgres/config.go index 72fcf37..66b083e 100644 --- a/pkg/postgres/config.go +++ b/pkg/postgres/config.go @@ -8,6 +8,23 @@ import ( "gorm.io/gorm" ) +type Client struct { + logger *zap.Logger + gormClient *gorm.DB +} + +func NewGormClient(dsn string, logger *zap.Logger) *gorm.DB { + // todo: set the connection configs + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + logger.Error("database connection failed", zap.Error(err)) + os.Exit(1) + } + + logger.Info("database connection established") + return db +} + func PQConnection(logger *zap.Logger) *gorm.DB { dsn := "" db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) diff --git a/pkg/postgres/query/auto_increment_id.go b/pkg/postgres/query/auto_increment_id.go deleted file mode 100644 index 576ae05..0000000 --- a/pkg/postgres/query/auto_increment_id.go +++ /dev/null @@ -1,18 +0,0 @@ -package query - -import ( - "sync" - - "gorm.io/gorm" -) - -type AutoIncrementID struct { - mutex sync.Mutex -} - -func (ai *AutoIncrementID) Next(db *gorm.DB) (int, error) { - ai.mutex.Lock() - defer ai.mutex.Unlock() - id, err := FindLatestIncidentId(db) - return (id + 1), err -} \ No newline at end of file diff --git a/pkg/postgres/query/contributing_factor.go b/pkg/postgres/query/contributing_factor.go deleted file mode 100644 index 38c3a76..0000000 --- a/pkg/postgres/query/contributing_factor.go +++ /dev/null @@ -1,71 +0,0 @@ -package query - -import ( - "houston/entity" - "time" - - "gorm.io/gorm" -) - -func FindContributingFactorsByIncidentId(db *gorm.DB, incidentId int) (*entity.ContributingFactorEntity, error) { - - var cf entity.ContributingFactorEntity - result := db.Where("incidents_tags_contributing_factor_mapping.incident_id = ? AND contributing_factor.deleted_at is NULL AND incidents_tags_contributing_factor_mapping.deleted_at is NULL", incidentId).Joins("JOIN incidents_tags_contributing_factor_mapping on incidents_tags_contributing_factor_mapping.contributing_factor_id = contributing_factor.id").Find(&cf) - if result.Error != nil { - return nil, result.Error - } - if result.RowsAffected == 0 { - return nil, nil - } - return &cf, nil -} - -func FetchAllContributingFactors(db *gorm.DB) ([]entity.ContributingFactorEntity, error) { - var cf []entity.ContributingFactorEntity - result := db.Where("deleted_at is NULL").Find(&cf) - if result.Error != nil { - return nil, result.Error - } - return cf, nil -} - -func UpsertContributingFactorIdToIncidentId(db *gorm.DB, cfId int, incidentId int) error { - - incidentTagsContributingFactorMapping := &entity.IncidentTagsContributingFactorMapping{ - ContributingFactorId: cfId, - IncidentId: incidentId, - } - var cf entity.IncidentTagsContributingFactorMapping - result := db.Where("incident_id = ? and contributing_factor_id = ? and deleted_at is NULL", incidentId, cfId).Find(&cf) - - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 1 { - cf.ContributingFactorId = cfId - cf.UpdatedAt = time.Now() - result := db.Save(cf) - if result != nil { - return result.Error - } - return nil - } else if result.RowsAffected == 0 { - addResult := db.Create(incidentTagsContributingFactorMapping) - if addResult != nil { - return addResult.Error - } - return nil - } - return gorm.ErrInvalidData - -} - -func SetContributingFactorDeletedAt(db *gorm.DB, incidentId int) error { - var cf entity.IncidentTagsContributingFactorMapping - result := db.Model(&cf).Where(" incident_id = ?", incidentId).Update("deleted_at", time.Now()) - - if result.Error != nil { - return result.Error - } - return nil -} diff --git a/pkg/postgres/query/customer_tags.go b/pkg/postgres/query/customer_tags.go deleted file mode 100644 index 0e1ff0e..0000000 --- a/pkg/postgres/query/customer_tags.go +++ /dev/null @@ -1,63 +0,0 @@ -package query - -import ( - "houston/entity" - "time" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -func FindCustomerTagsByIncidentId(logger *zap.Logger, db *gorm.DB, incidentId int) ([]entity.CustomerTagsEntity, error) { - - var customerTags []entity.CustomerTagsEntity - result := db.Where("incidents_tags_customer_mapping.incident_id = ? AND customer_tags.deleted_at is NULL AND incidents_tags_customer_mapping.deleted_at is NULL", incidentId).Joins("JOIN incidents_tags_customer_mapping on incidents_tags_customer_mapping.customer_tags_id = customer_tags.id").Find(&customerTags) - if result.Error != nil { - return nil, result.Error - } - return customerTags, nil -} - -func FetchAllCustomerTags(db *gorm.DB) ([]entity.CustomerTagsEntity, error) { - var customerTags []entity.CustomerTagsEntity - result := db.Where("deleted_at is NULL").Find(&customerTags) - if result.Error != nil { - return nil, result.Error - } - return customerTags, nil -} - -func FindCustomerTagIdMappingWithIncidentByIncidentId(db *gorm.DB, incidentId int) ([]entity.IncidentTagsCustomerMapping, error) { - var incidentTagsCustomerMapping []entity.IncidentTagsCustomerMapping - result := db.Where("incident_id = ? AND deleted_at is NULL", incidentId).Find(&incidentTagsCustomerMapping) - if result.Error != nil { - return nil, result.Error - } - return incidentTagsCustomerMapping, nil -} - -func SetIncidentCustomerTagDeletedAt(db *gorm.DB, customerTagId int, incidentId int) error { - var incidentTagsCustomerMapping entity.IncidentTagsCustomerMapping - result := db.Model(&incidentTagsCustomerMapping).Where("customer_tags_id = ? AND incident_id = ?", customerTagId, incidentId).Update("deleted_at", time.Now()) - - if result.Error != nil { - return result.Error - } - return nil - -} - -func AddCustomerIdMappingToIncidentId(db *gorm.DB, customerTagId int, incidentId int) error { - var incidentTagsCustomerMapping = &entity.IncidentTagsCustomerMapping{ - IncidentId: incidentId, - CustomerTagsId: customerTagId, - } - - result := db.Create(incidentTagsCustomerMapping) - - if result.Error != nil { - return result.Error - } - return nil - -} diff --git a/pkg/postgres/query/incident.go b/pkg/postgres/query/incident.go deleted file mode 100644 index 5944199..0000000 --- a/pkg/postgres/query/incident.go +++ /dev/null @@ -1,143 +0,0 @@ -package query - -import ( - "fmt" - "houston/entity" - "houston/model" - "strconv" - "time" - - "gorm.io/gorm" -) - -func CreateIncident(db *gorm.DB, request *model.CreateIncident) (*entity.IncidentEntity, error) { - severityId, err := strconv.Atoi(request.IncidentSeverity) - if err != nil { - return nil, fmt.Errorf("fetch channel conversationInfo failed. err: %v", err) - } - severity, err := FindSeverityById(db, severityId) - if err != nil { - return nil, fmt.Errorf("fetch FindSeverityById failed. err: %v", err) - } - - incidentEntity := &entity.IncidentEntity{ - Title: request.IncidentTitle, - Description: request.IncidentDescription, - Status: request.Status, - SeverityId: severityId, - IncidentName: request.IncidentName, - SlackChannel: request.SlackChannel, - DetectionTime: request.DetectionTime, - CustomerImpactStartTime: request.CustomerImpactStartTime, - CustomerImpactEndTime: request.CustomerImpactEndTime, - TeamsId: request.TeamsId, - JiraId: request.JiraId, - ConfluenceId: request.ConfluenceId, - RemindMeAt: request.RemindMeAt, - EnableReminder: request.EnableReminder, - SeverityTat: time.Now().AddDate(0, 0, severity.Sla), - CreatedBy: request.CreatedBy, - UpdatedBy: request.UpdatedBy, - Version: request.Version, - } - - result := db.Create(incidentEntity) - if result.Error != nil { - return nil, result.Error - } - - return incidentEntity, nil -} - -func UpdateIncident(db *gorm.DB, incidentEntity *entity.IncidentEntity) error { - result := db.Updates(incidentEntity) - if result.Error != nil { - return result.Error - } - - return nil -} -func FindIncidentById(db *gorm.DB, incidentId string) (*entity.IncidentEntity, error) { - var incidentEntity entity.IncidentEntity - - result := db.Find(&incidentEntity, "id = ?", incidentId) - if result.Error != nil { - return nil, result.Error - } - - return &incidentEntity, nil -} - -func FindNotResolvedLatestIncidents(db *gorm.DB, limit int) ([]entity.IncidentSeverityTeamJoinEntity, error) { - var incidentSeverityTeamJoinEntity []entity.IncidentSeverityTeamJoinEntity - - result := db.Limit(limit). - Where("status <> ? AND incidents.deleted_at IS NULL", entity.Resolved). - Order("incidents.created_at desc"). - Joins("JOIN severity ON severity.id = incidents.severity_id"). - Joins("JOIN teams ON teams.id = incidents.teams_id"). - Select("incidents.title,incidents.status,incidents.slack_channel,severity.id as severity_id,severity.name as severity_name,teams.id as teams_id,teams.name as teams_name"). - Find(&entity.IncidentEntity{}). - Scan(&incidentSeverityTeamJoinEntity) - - if result.Error != nil { - return nil, result.Error - } - - return incidentSeverityTeamJoinEntity, nil -} - -func FindIncidentByChannelId(db *gorm.DB, channelId string) (*entity.IncidentEntity, error) { - var incidentEntity entity.IncidentEntity - - result := db.Find(&incidentEntity, "slack_channel = ?", channelId) - if result.Error != nil { - return nil, result.Error - } - - if result.RowsAffected == 0 { - return nil, nil - } - - return &incidentEntity, nil -} - -func FindIncidentSeverityTeamJoin(db *gorm.DB, slackChannelId string) (*entity.IncidentSeverityTeamJoinEntity, error) { - var incidentSeverityTeamJoinEntity entity.IncidentSeverityTeamJoinEntity - - result := db.Where("incidents.slack_channel = ? and incidents.deleted_at IS NULL", slackChannelId).Joins("JOIN severity ON severity.id = incidents.severity_id").Joins("JOIN teams ON teams.id = incidents.teams_id").Select("incidents.id as incident_id,incidents.title,incidents.status,incidents.slack_channel,severity.id as severity_id,severity.name as severity_name,teams.id as teams_id,teams.name as teams_name").Find(&entity.IncidentEntity{}).Scan(&incidentSeverityTeamJoinEntity) - - if result.Error != nil { - return nil, result.Error - } - if result.RowsAffected == 0 { - return nil, nil - } - - return &incidentSeverityTeamJoinEntity, nil -} - -func FindLatestIncidentId(db *gorm.DB) (int, error) { - var incidentEntity entity.IncidentEntity - - result := db.Order("incidents.id desc").Limit(1).Find(&incidentEntity) - - if result.Error != nil { - return -1, result.Error - } - - return int(incidentEntity.ID), nil -} - -func FindIncidentsBreachingSevTat(db *gorm.DB) ([]entity.IncidentEntity, error) { - var incidentEntity []entity.IncidentEntity - - currentTime := time.Now() - result := db.Where("status <> ? AND severity_tat <= ?", entity.Resolved, currentTime).Find(&incidentEntity) - - if result.Error != nil { - return nil, result.Error - } - - return incidentEntity, nil -} diff --git a/pkg/postgres/query/incident_role.go b/pkg/postgres/query/incident_role.go deleted file mode 100644 index 9bc3f46..0000000 --- a/pkg/postgres/query/incident_role.go +++ /dev/null @@ -1,42 +0,0 @@ -package query - -import ( - "houston/entity" - "houston/model/request" - "time" - - "gorm.io/gorm" -) - -func UpsertIncidentRole(db *gorm.DB, addIncidnentRoleRequest *request.AddIncidentRoleRequest) error { - incidentRolesEntity := &entity.IncidentRoles{ - IncidentId: addIncidnentRoleRequest.IncidentId, - Role: addIncidnentRoleRequest.Role, - AssignedToUserSlackId: addIncidnentRoleRequest.UserId, - AssignedByUserSlackId: addIncidnentRoleRequest.CreatedById, - } - var incidentRoles entity.IncidentRoles - - result := db.Find(&incidentRoles, "incident_id = ? AND role = ?", addIncidnentRoleRequest.IncidentId, addIncidnentRoleRequest.Role) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 1 { - incidentRolesEntity.ID = incidentRoles.ID - incidentRolesEntity.CreatedAt = incidentRoles.CreatedAt - incidentRolesEntity.UpdatedAt = time.Now() - addResult := db.Save(incidentRolesEntity) - if addResult != nil { - return addResult.Error - } - return nil - - } else if result.RowsAffected == 0 { - addResult := db.Create(incidentRolesEntity) - if addResult != nil { - return addResult.Error - } - return nil - } - return gorm.ErrInvalidData -} diff --git a/pkg/postgres/query/incident_status.go b/pkg/postgres/query/incident_status.go deleted file mode 100644 index b9de5a8..0000000 --- a/pkg/postgres/query/incident_status.go +++ /dev/null @@ -1,45 +0,0 @@ -package query - -import ( - "houston/entity" - "houston/model/request" - - "gorm.io/gorm" -) - -func CreateIncidentStatus(db *gorm.DB, request *request.AddIncidentStatusRequest) error { - incidentStatusEntity := &entity.IncidentStatusEntity{ - Name: request.Name, - Description: request.Description, - } - - result := db.Create(incidentStatusEntity) - if result.Error != nil { - return result.Error - } - - return nil -} - -func FindIncidentStatusById(db *gorm.DB, incidentStatusId int) (*entity.IncidentStatusEntity, error) { - var incidentStatusEntity entity.IncidentStatusEntity - - result := db.Find(&incidentStatusEntity, "id = ?", incidentStatusId) - if result.Error != nil { - return nil, result.Error - } - if result.RowsAffected == 0 { - return nil, nil - } - - return &incidentStatusEntity, nil -} - -func FetchAllIncidentStatus(db *gorm.DB) ([]entity.IncidentStatusEntity, error) { - var incidentStatusEntity []entity.IncidentStatusEntity - result := db.Find(&incidentStatusEntity) - if result.Error != nil { - return nil, result.Error - } - return incidentStatusEntity, nil -} diff --git a/pkg/postgres/query/incidents_tags_data_platform_mapping.go b/pkg/postgres/query/incidents_tags_data_platform_mapping.go deleted file mode 100644 index a0e9724..0000000 --- a/pkg/postgres/query/incidents_tags_data_platform_mapping.go +++ /dev/null @@ -1,52 +0,0 @@ -package query - -import ( - "houston/entity" - "time" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -func FindDataPlataformTagsByIncidentId(incidenttId int, db *gorm.DB, logger *zap.Logger) (*entity.IncidentsTagsDataPlatformMapping, error) { - - var cf entity.IncidentsTagsDataPlatformMapping - result := db.Where("incidents_tags_data_platform_mapping.incident_id = ? AND incidents_tags_data_platform_mapping.deleted_at is NULL", incidenttId).Find(&cf) - if result.Error != nil { - return nil, result.Error - } - if result.RowsAffected == 0 { - return nil, nil - } - return &cf, nil -} - -func UpsertDataPlatformTagToIncidentId(db *gorm.DB, dataPlatform string, incidentId int) error { - dpEntity := &entity.IncidentsTagsDataPlatformMapping{ - DataPlatformTag: dataPlatform, - IncidentId: incidentId, - } - var dp entity.IncidentsTagsDataPlatformMapping - result := db.Where("incident_id = ? and deleted_at is NULL", incidentId).Find(&dp) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 1 { - dp.DataPlatformTag = dataPlatform - dp.UpdatedAt = time.Now() - addResult := db.Save(dp) - if addResult != nil { - return addResult.Error - } - - return nil - } else if result.RowsAffected == 0 { - addResult := db.Create(dpEntity) - if addResult != nil { - return addResult.Error - } - return nil - } - return gorm.ErrInvalidData - -} diff --git a/pkg/postgres/query/message_color_code.go b/pkg/postgres/query/message_color_code.go deleted file mode 100644 index 98f2e4c..0000000 --- a/pkg/postgres/query/message_color_code.go +++ /dev/null @@ -1,14 +0,0 @@ -package query - -func GetColorBySeverity(severity_id int) (string) { - switch severity_id { - case 1 : - return "#fc3838" - case 2 : - return "#fc9338" - case 3 : - return "#ebfa1b" - default : - return "#7288db" - } -} \ No newline at end of file diff --git a/pkg/postgres/query/messages.go b/pkg/postgres/query/messages.go deleted file mode 100644 index 0a21f93..0000000 --- a/pkg/postgres/query/messages.go +++ /dev/null @@ -1,38 +0,0 @@ -package query - -import ( - "houston/entity" - "houston/model/request" - "time" - - "gorm.io/gorm" -) - -func CreateMessage(db *gorm.DB, request *request.CreateMessage) (error) { - messageEntity := &entity.MessageEntity { - SlackChannel: request.SlackChannel, - MessageTimeStamp: request.MessageTimeStamp, - IncidentName: request.IncidentName, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Version: 0, - } - - result := db.Create(&messageEntity) - if result.Error != nil { - return result.Error - } - - return nil -} - -func FindMessageByIncidentName(db *gorm.DB, incidentName string) ([]entity.MessageEntity, error) { - var messages []entity.MessageEntity - - result := db.Find(&messages, "incident_name = ?", incidentName) - if result.Error != nil { - return nil, result.Error - } - - return messages, nil -} diff --git a/pkg/postgres/query/severity.go b/pkg/postgres/query/severity.go deleted file mode 100644 index e87fa11..0000000 --- a/pkg/postgres/query/severity.go +++ /dev/null @@ -1,72 +0,0 @@ -package query - -import ( - "houston/entity" - "houston/model/request" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -func FindSeverity(db *gorm.DB, logger *zap.Logger) ([]entity.SeverityEntity, error) { - var severityEntity []entity.SeverityEntity - result := db.Where("deleted_at is NULL").Find(&severityEntity) - if result.Error != nil { - logger.Error("fetching severity query failed", zap.Error(result.Error)) - return nil, result.Error - } - return severityEntity, nil -} - -func FindIncidentSeverityEntity(db *gorm.DB, logger *zap.Logger) ([]entity.SeverityEntity, error) { - var severityEntity []entity.SeverityEntity - - result := db.Where("deleted_at is NULL").Find(&severityEntity) - if result.Error != nil { - logger.Error("fetching severity query failed", zap.Error(result.Error)) - return nil, result.Error - } - - return severityEntity, nil -} - -func FindIncidentSeverityEntityById(db *gorm.DB, logger *zap.Logger, id int) (*entity.SeverityEntity, error) { - var severityEntity entity.SeverityEntity - - result := db.Find(&severityEntity, "id = ?", id) - if result.Error != nil { - logger.Error("fetching severity query failed", zap.Error(result.Error)) - return nil, result.Error - } else if result.RowsAffected == 0 { - logger.Error("SeverityEntity not found", zap.Error(result.Error)) - return nil, nil - } - - return &severityEntity, nil -} - -func AddSeverity(db *gorm.DB, logger *zap.Logger, addSeverityRequest request.AddSeverityRequest) error { - severityEntity := &entity.SeverityEntity{ - Name: addSeverityRequest.Name, - Description: addSeverityRequest.Description, - } - - result := db.Create(severityEntity) - if result.Error != nil { - logger.Error("failed to add severity in the database", zap.Error(result.Error)) - return result.Error - } - - return nil -} - -func FindSeverityById(db *gorm.DB, severityId int) (*entity.SeverityEntity, error) { - var severityEntity entity.SeverityEntity - - result := db.Find(&severityEntity, "id = ?", severityId) - if result.Error != nil { - return nil, result.Error - } - - return &severityEntity, nil -} diff --git a/pkg/postgres/query/tags.go b/pkg/postgres/query/tags.go deleted file mode 100644 index 6d6c5a5..0000000 --- a/pkg/postgres/query/tags.go +++ /dev/null @@ -1,121 +0,0 @@ -package query - -import ( - "houston/entity" - "time" - - "gorm.io/gorm" -) - -func FindTagsByTeamsId(db *gorm.DB, teamsId int) ([]entity.TagsEntity, error) { - var tags []entity.TagsEntity - - result := db.Where("teams_tags_mapping.teams_id = ? AND teams_tags_mapping.deleted_at IS NULL", teamsId).Joins("JOIN teams_tags_mapping ON tags.id = teams_tags_mapping.tag_id").Find(&tags) - - if result.Error != nil { - return nil, result.Error - } - - if result.RowsAffected == 0 { - return nil, nil - } - - return tags, nil -} - -func FindTagsByIncidentId(db *gorm.DB, incidentId int) ([]entity.TagsEntity, error) { - var tags []entity.TagsEntity - - result := db.Where("incidents_tags_mapping.incident_id = ? AND incidents_tags_mapping.deleted_at IS NULL", incidentId).Joins("JOIN incidents_tags_mapping ON incidents_tags_mapping.tag_id = tags.id").Find(&tags) - - if result.Error != nil { - return nil, result.Error - } - - if result.RowsAffected == 0 { - return nil, nil - } - - return tags, nil -} - -func AddTagsIdMappingToTeamsId(db *gorm.DB, tagsId int, teamsId int) error { - teamsTagsMapping := &entity.TeamTagsMapping{ - TeamsId: teamsId, - TagId: tagsId, - } - result := db.Create(teamsTagsMapping) - - if result.Error != nil { - return result.Error - } - return nil - -} - -func FindTagRecordByTeamsIdAndTagsId(db *gorm.DB, tagsId int, teamsId int) (*entity.TagsEntity, error) { - var tagsMapping entity.TagsEntity - - result := db.Where("tagsId = ? AND teamsId = ? AND deleted_at IS NULL", tagsId, teamsId).Joins("JOIN teams_tags_mapping ON tags.id = teams_tags_mapping.tags_id").Find(tagsMapping) - - if result.Error != nil { - return nil, result.Error - } - return &tagsMapping, nil - -} - -func FindTagRecordListByIncidentId(db *gorm.DB, incidenId int) ([]entity.IncidentsTagsMapping, error) { - var incidentsTagsMapping []entity.IncidentsTagsMapping - - result := db.Where("incident_id = ? AND deleted_at IS NULL", incidenId).Find(&incidentsTagsMapping) - - if result.Error != nil { - return nil, result.Error - } - return incidentsTagsMapping, nil - -} - -func AddTagsIdMappingToIncidentId(db *gorm.DB, tagId, incidenId int) error { - var incidentsTagsMapping = &entity.IncidentsTagsMapping{ - IncidentId: incidenId, - TagId: tagId, - } - - result := db.Create(incidentsTagsMapping) - - if result.Error != nil { - return result.Error - } - return nil - -} - -func SetTagsDeletedAt(db *gorm.DB, tagId int, resultId int) error { - - var incidentTags entity.IncidentsTagsMapping - result := db.Model(&incidentTags).Where("tag_id = ? AND incident_id = ?", tagId, resultId).Update("deleted_at", time.Now()) - - if result.Error != nil { - return result.Error - } - return nil - -} - -func FindTagsById(db *gorm.DB, tagId int) (*entity.TagsEntity, error) { - var tagsEntity entity.TagsEntity - - result := db.Where("id = ? AND deleted_at IS NULL", tagId).Find(tagsEntity) - - if result.Error != nil { - return nil, result.Error - } - - if result.RowsAffected == 0 { - return nil, nil - } - return &tagsEntity, nil - -} diff --git a/pkg/postgres/query/teams.go b/pkg/postgres/query/teams.go deleted file mode 100644 index d512c8c..0000000 --- a/pkg/postgres/query/teams.go +++ /dev/null @@ -1,66 +0,0 @@ -package query - -import ( - "houston/entity" - "houston/model/request" - - "gorm.io/gorm" -) - -func FindTeam(db *gorm.DB) ([]entity.TeamEntity, error) { - var teamEntity []entity.TeamEntity - - result := db.Find(&teamEntity, "active = ?", true) - if result.Error != nil { - return nil, result.Error - } - return teamEntity, nil -} - -func FindTeamList(db *gorm.DB) ([]entity.TeamEntity, error) { - var teamEntity []entity.TeamEntity - result := db.Find(&teamEntity, "active = ?", true) - if result.Error != nil { - return nil, result.Error - } - - return teamEntity, nil -} - -func FindTeamByName(db *gorm.DB, name string) (*entity.TeamEntity, error) { - var teamEntity entity.TeamEntity - result := db.Find(&teamEntity, "name = ?", name) - if result.Error != nil { - return nil, result.Error - } - - return &teamEntity, nil -} - -func AddTeam(db *gorm.DB, addTeamRequest request.AddTeamRequest) error { - teamEntity := &entity.TeamEntity{ - Name: addTeamRequest.Name, - OncallHandle: addTeamRequest.OncallHandle, - } - - result := db.Create(teamEntity) - if result.Error != nil { - return result.Error - } - - return nil -} - -func FindTeamById(db *gorm.DB, teamId int) (*entity.TeamEntity, error) { - var teamEntity entity.TeamEntity - - result := db.Find(&teamEntity, "id = ?", teamId) - if result.Error != nil { - return nil, result.Error - } else if result.RowsAffected == 0 { - return nil, nil - - } - - return &teamEntity, nil -} diff --git a/pkg/postgres/query/users.go b/pkg/postgres/query/users.go deleted file mode 100644 index 77d074c..0000000 --- a/pkg/postgres/query/users.go +++ /dev/null @@ -1,35 +0,0 @@ -package query - -import ( - "houston/entity" - - "gorm.io/gorm" -) - -func FindDefaultUserIdToBeAddedBySeverity(db *gorm.DB, severityId int) ([]string, error) { - userIds := make([]string, 0) - var user []entity.UsersEntity - result := db.Where("users.active = true And teams_severity_user_mapping.deleted_at is NULL AND teams_severity_user_mapping.default_add_in_incidents = ? AND teams_severity_user_mapping.entity_type = ? AND teams_severity_user_mapping.entity_id = ?", true, entity.SEVERITY, severityId).Joins("JOIN teams_severity_user_mapping on teams_severity_user_mapping.users_id = users.id").Find(&user) - if result.Error != nil { - return nil, result.Error - } - for _, users := range user { - userIds = append(userIds, users.SlackUserId) - } - - return userIds, nil -} - -func FindDefaultUserIdToBeAddedByTeam(db *gorm.DB, teamId int) ([]string, error) { - userIds := make([]string, 0) - var user []entity.UsersEntity - result := db.Where("users.active = true And teams_severity_user_mapping.deleted_at is NULL AND teams_severity_user_mapping.default_add_in_incidents = ? AND teams_severity_user_mapping.entity_type = ? AND teams_severity_user_mapping.entity_id = ?", true, entity.TEAM, teamId).Joins("JOIN teams_severity_user_mapping on teams_severity_user_mapping.users_id = users.id").Find(&user) - if result.Error != nil { - return nil, result.Error - } - for _, users := range user { - userIds = append(userIds, users.SlackUserId) - } - - return userIds, nil -} diff --git a/pkg/postgres/service/incident/entity.go b/pkg/postgres/service/incident/entity.go new file mode 100644 index 0000000..eb900c4 --- /dev/null +++ b/pkg/postgres/service/incident/entity.go @@ -0,0 +1,102 @@ +package incident + +import ( + "github.com/lib/pq" + "time" + + "gorm.io/gorm" +) + +type IncidentStatus string + +const ( + Investigating IncidentStatus = "Investigating" + Identified = "Identified" + Monitoring = "Monitoring" + Resolved = "Resolved" + Duplicated = "Duplicated" +) + +const ( + Retrospective IncidentRole = "Retrospective" + Responder = "Responder" + ServiceOwner = "Service Owner" +) + +type IncidentRole string + +// IncidentEntity all the incident created will go in this table +type IncidentEntity struct { + gorm.Model + Title string `gorm:"column:title"` + Description string `gorm:"column:description"` + Status uint `gorm:"column:status"` + SeverityId uint `gorm:"column:severity_id"` + IncidentName string `gorm:"column:incident_name"` + SlackChannel string `gorm:"column:slack_channel"` + DetectionTime *time.Time `gorm:"column:detection_time"` + StartTime time.Time `gorm:"column:start_time"` + EndTime *time.Time `gorm:"column:end_time"` + TeamId uint `gorm:"column:team_id"` + JiraId *string `gorm:"column:jira_id"` + ConfluenceId *string `gorm:"column:confluence_id"` + SeverityTat time.Time `gorm:"column:severity_tat"` + RemindMeAt *time.Time `gorm:"column:remind_me_at"` + EnableReminder bool `gorm:"column:enable_reminder"` + CreatedBy string `gorm:"column:created_by"` + UpdatedBy string `gorm:"column:updated_by"` +} + +func (IncidentEntity) TableName() string { + return "incident" +} + +// IncidentRoleEntity this table will contain the role of the people assigned to the incident, mapping between incident +// and incident role entity will be one to many +type IncidentRoleEntity struct { + gorm.Model + IncidentId int `gorm:"column:incident_id"` + Role IncidentRole `gorm:"column:role"` + AssignedTo string `gorm:"column:assigned_to"` + AssignedBy string `gorm:"column:assigned_by"` +} + +func (IncidentRoleEntity) TableName() string { + return "incident_role" +} + +// IncidentChannelEntity contains the channels where the incident is being tracked +type IncidentChannelEntity struct { + gorm.Model + IncidentId uint `gorm:"column:incident_id"` + SlackChannel string `gorm:"column:slack_channel"` + MessageTimeStamp string `gorm:"column:message_timestamp"` +} + +func (IncidentChannelEntity) TableName() string { + return "incident_channel" +} + +// IncidentStatusEntity contains the possible incident statuses +type IncidentStatusEntity struct { + gorm.Model + Name string `gorm:"column:name"` + Description string `gorm:"column:description"` + IsTerminalStatus bool `gorm:"column:is_terminal_status"` +} + +func (IncidentStatusEntity) TableName() string { + return "incident_status" +} + +type IncidentTagEntity struct { + gorm.Model + IncidentId uint `gorm:"column:incident_id"` + TagId uint `gorm:"column:tag_id"` + TagValueIds pq.Int32Array `gorm:"column:tag_value_ids;type:integer[]"` + FreeTextValue *string `gorm:"column:free_text_value"` +} + +func (IncidentTagEntity) TableName() string { + return "incident_tag" +} diff --git a/pkg/postgres/service/incident/incident.go b/pkg/postgres/service/incident/incident.go new file mode 100644 index 0000000..60f8082 --- /dev/null +++ b/pkg/postgres/service/incident/incident.go @@ -0,0 +1,294 @@ +package incident + +import ( + "fmt" + "go.uber.org/zap" + "gorm.io/gorm" + "houston/pkg/postgres/service/severity" + "strconv" + "time" +) + +type Service struct { + logger *zap.Logger + gormClient *gorm.DB + severityService *severity.Service +} + +func NewIncidentService(logger *zap.Logger, gormClient *gorm.DB, severityService *severity.Service) *Service { + return &Service{ + logger: logger, + gormClient: gormClient, + severityService: severityService, + } +} + +func (s *Service) CreateIncident(request *CreateIncidentRequest) (*IncidentEntity, error) { + severityId, err := strconv.Atoi(request.Severity) + if err != nil { + return nil, fmt.Errorf("fetch channel conversationInfo failed. err: %v", err) + } + severity, err := s.severityService.FindSeverityById(uint(severityId)) + if err != nil { + return nil, fmt.Errorf("fetch FindSeverityById failed. err: %v", err) + } + + teamId, _ := strconv.Atoi(request.TeamId) + incidentStatusEntity, _ := s.GetIncidentStatusByStatusName(string(request.Status)) + + incidentEntity := &IncidentEntity{ + Title: request.Title, + Description: request.Description, + Status: incidentStatusEntity.ID, + SeverityId: uint(severityId), + DetectionTime: request.DetectionTime, + StartTime: request.StartTime, + TeamId: uint(teamId), + EnableReminder: request.EnableReminder, + SeverityTat: time.Now().AddDate(0, 0, severity.Sla), + CreatedBy: request.CreatedBy, + UpdatedBy: request.UpdatedBy, + } + + result := s.gormClient.Create(incidentEntity) + if result.Error != nil { + return nil, result.Error + } + + return incidentEntity, nil +} + +func (s *Service) UpdateIncident(incidentEntity *IncidentEntity) error { + result := s.gormClient.Updates(incidentEntity) + if result.Error != nil { + return result.Error + } + + return nil +} + +func (s *Service) GetIncidentStatusByStatusName(status string) (*IncidentStatusEntity, error) { + var incidentStatus IncidentStatusEntity + + result := s.gormClient.Find(&incidentStatus, "name = ?", status) + if result.Error != nil { + return nil, result.Error + } + if result.RowsAffected == 0 { + return nil, nil + } + + return &incidentStatus, nil +} + +func (s *Service) GetOpenIncidents(limit int) (*[]IncidentSeverityTeamDTO, error) { + var incidentSeverityTeamDTO []IncidentSeverityTeamDTO + + result := s.gormClient.Raw(` + select i.title, ins.name as status, i.slack_channel, s.name as severity_name, t.name as team_name + from incident i + inner join severity s on s.id = i.severity_id + inner join team t on t.id = i.team_id + inner join incident_status ins on ins.id = i.status + where ins.name <> ? and i.deleted_at is null + limit ? + `, Resolved, limit).Scan(&incidentSeverityTeamDTO) + + if result.Error != nil { + return nil, result.Error + } + if result.RowsAffected == 0 { + return nil, nil + } + + return &incidentSeverityTeamDTO, nil +} + +func (s *Service) FindIncidentByChannelId(channelId string) (*IncidentEntity, error) { + var incidentEntity IncidentEntity + + result := s.gormClient.Find(&incidentEntity, "slack_channel = ?", channelId) + if result.Error != nil { + return nil, result.Error + } + + if result.RowsAffected == 0 { + return nil, nil + } + + return &incidentEntity, nil +} + +func (s *Service) FindIncidentSeverityTeamJoin(slackChannelId string) (*IncidentSeverityTeamDTO, error) { + var incidentSeverityTeamJoinEntity IncidentSeverityTeamDTO + + result := s.gormClient.Table("incident"). + Where("incident.slack_channel = ? and incident.deleted_at IS NULL", slackChannelId). + Joins("JOIN severity ON severity.id = incident.severity_id"). + Joins("JOIN team ON team.id = incident.team_id"). + Joins("JOIN incident_status on incident.status = incident_status.id"). + Select("incident.title, incident_status.name as status, incident.slack_channel, severity.name as severity_name ,team.name as team_name"). + Scan(&incidentSeverityTeamJoinEntity) + + if result.Error != nil { + return nil, result.Error + } + if result.RowsAffected == 0 { + return nil, nil + } + + return &incidentSeverityTeamJoinEntity, nil +} + +func (s *Service) UpsertIncidentRole(addIncidentRoleRequest *AddIncidentRoleRequest) error { + incidentRolesEntity := &IncidentRoleEntity{ + IncidentId: addIncidentRoleRequest.IncidentId, + Role: addIncidentRoleRequest.Role, + AssignedTo: addIncidentRoleRequest.UserId, + AssignedBy: addIncidentRoleRequest.CreatedById, + } + var incidentRole IncidentRoleEntity + + result := s.gormClient.Find(&incidentRole, "incident_id = ? AND role = ?", addIncidentRoleRequest.IncidentId, addIncidentRoleRequest.Role) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 1 { + incidentRolesEntity.ID = incidentRole.ID + incidentRolesEntity.CreatedAt = incidentRole.CreatedAt + incidentRolesEntity.UpdatedAt = time.Now() + addResult := s.gormClient.Save(incidentRolesEntity) + if addResult != nil { + return addResult.Error + } + return nil + + } else if result.RowsAffected == 0 { + addResult := s.gormClient.Create(incidentRolesEntity) + if addResult != nil { + return addResult.Error + } + return nil + } + return gorm.ErrInvalidData +} + +func (s *Service) FindIncidentStatusById(incidentStatusId uint) (*IncidentStatusEntity, error) { + var incidentStatusEntity IncidentStatusEntity + + result := s.gormClient.Find(&incidentStatusEntity, "id = ?", incidentStatusId) + if result.Error != nil { + return nil, result.Error + } + if result.RowsAffected == 0 { + return nil, nil + } + + return &incidentStatusEntity, nil +} + +func (s *Service) FindIncidentStatusByName(name string) (*IncidentStatusEntity, error) { + var incidentStatusEntity IncidentStatusEntity + + result := s.gormClient.Find(&incidentStatusEntity, "name = ?", name) + if result.Error != nil { + return nil, result.Error + } + if result.RowsAffected == 0 { + return nil, nil + } + + return &incidentStatusEntity, nil +} + +func (s *Service) FetchAllIncidentStatuses() (*[]IncidentStatusEntity, error) { + var incidentStatusEntity []IncidentStatusEntity + result := s.gormClient.Find(&incidentStatusEntity) + if result.Error != nil { + return nil, result.Error + } + + if result.RowsAffected == 0 { + return nil, nil + } + + return &incidentStatusEntity, nil +} + +func (s *Service) CreateIncidentChannelEntry(request *CreateIncidentChannelEntry) error { + messageEntity := &IncidentChannelEntity{ + SlackChannel: request.SlackChannel, + MessageTimeStamp: request.MessageTimeStamp, + IncidentId: request.IncidentId, + } + + result := s.gormClient.Create(&messageEntity) + if result.Error != nil { + return result.Error + } + + return nil +} + +func (s *Service) CreateIncidentTag(incidentId, tagId uint) (*IncidentTagEntity, error) { + incidentTag := IncidentTagEntity{ + IncidentId: incidentId, + TagId: tagId, + } + + result := s.gormClient.Create(&incidentTag) + if result.Error != nil { + return nil, result.Error + } + return &incidentTag, nil +} + +func (s *Service) GetIncidentChannels(incidentId uint) (*[]IncidentChannelEntity, error) { + var incidentChannels []IncidentChannelEntity + + result := s.gormClient.Find(&incidentChannels, "incident_id = ?", incidentId) + if result.Error != nil { + return nil, result.Error + } + + if result.RowsAffected == 0 { + return nil, nil + } + + return &incidentChannels, nil +} + +func (s *Service) GetIncidentTagsByIncidentId(incidentId uint) (*[]IncidentTagEntity, error) { + var incidentTags []IncidentTagEntity + + result := s.gormClient.Find(&incidentTags, "incident_id = ?", incidentId) + if result.Error != nil { + return nil, result.Error + } + if result.RowsAffected == 0 { + return nil, nil + } + + return &incidentTags, nil +} + +func (s *Service) GetIncidentTagByTagId(incidentId uint, tagId uint) (*IncidentTagEntity, error) { + var incidentTag IncidentTagEntity + + result := s.gormClient.Find(&incidentTag, "incident_id = ? and tag_id = ?", incidentId, tagId) + if result.Error != nil { + return nil, result.Error + } else if result.RowsAffected == 0 { + return nil, nil + } + + return &incidentTag, nil +} + +func (s *Service) SaveIncidentTag(entity IncidentTagEntity) (*IncidentTagEntity, error) { + tx := s.gormClient.Save(&entity) + if tx.Error != nil { + return nil, tx.Error + } + return &entity, nil +} diff --git a/pkg/postgres/service/incident/model.go b/pkg/postgres/service/incident/model.go new file mode 100644 index 0000000..4231032 --- /dev/null +++ b/pkg/postgres/service/incident/model.go @@ -0,0 +1,52 @@ +package incident + +import ( + "time" +) + +type CreateIncidentRequest struct { + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Severity string `json:"severity,omitempty"` + Pagerduty string `json:"pagerduty,omitempty"` + Status IncidentStatus `json:"status,omitempty"` + IncidentName string `json:"incident_name,omitempty"` + SlackChannel string `json:"slack_channel,omitempty"` + DetectionTime *time.Time `json:"detection_time,omitempty"` + StartTime time.Time `json:"start_time,omitempty"` + EndTime *time.Time `json:"end_time,omitempty"` + TeamId string `json:"type,omitempty"` + JiraId *string `json:"jira_id,omitempty"` + ConfluenceId *string `json:"confluence_id,omitempty"` + SeverityTat *time.Time `json:"severity_tat,omitempty"` + RemindMeAt *time.Time `json:"remind_me_at,omitempty"` + EnableReminder bool `json:"enable_reminder,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + UpdatedBy string `json:"updated_by,omitempty"` +} + +type IncidentSeverityTeamDTO struct { + Title string + Status IncidentStatus + SeverityName string + SlackChannel string + TeamName string +} + +type AddIncidentRoleRequest struct { + UserId string `json:"users_select,omitempty"` + Role IncidentRole `json:"role_type,omitempty"` + IncidentId int + CreatedById string +} + +type CreateIncidentChannelEntry struct { + SlackChannel string `gorm:"column:slack_channel"` + IncidentId uint `gorm:"column:incident_id"` + MessageTimeStamp string `gorm:"column:message_timestamp"` +} + +type AddIncidentStatusRequest struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` +} diff --git a/pkg/postgres/service/role/entity.go b/pkg/postgres/service/role/entity.go new file mode 100644 index 0000000..821bbde --- /dev/null +++ b/pkg/postgres/service/role/entity.go @@ -0,0 +1,12 @@ +package role + +import "gorm.io/gorm" + +type RoleEntity struct { + gorm.Model + name string `gorm:"column:name"` +} + +func (RoleEntity) TableName() string { + return "role" +} diff --git a/pkg/postgres/service/role/role.go b/pkg/postgres/service/role/role.go new file mode 100644 index 0000000..99388dd --- /dev/null +++ b/pkg/postgres/service/role/role.go @@ -0,0 +1,34 @@ +package role + +import ( + "fmt" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type Service struct { + logger *zap.Logger + gormClient *gorm.DB +} + +func NewRoleRepository(logger *zap.Logger, gormClient *gorm.DB) *Service { + return &Service{ + logger: logger, + gormClient: gormClient, + } +} + +func (r *Service) FindRoleById(id int) (*RoleEntity, error) { + role := RoleEntity{} + tx := r.gormClient.Raw("select * from role where id = ?", id).Scan(&role) + + if tx.Error != nil { + r.logger.Error("Error while getting role", zap.String("exception", fmt.Sprintf("%v", tx.Error))) + return nil, tx.Error + } + if tx.RowsAffected == 0 { + return nil, nil + } + + return &role, nil +} diff --git a/pkg/postgres/service/severity/entity.go b/pkg/postgres/service/severity/entity.go new file mode 100644 index 0000000..37d14e6 --- /dev/null +++ b/pkg/postgres/service/severity/entity.go @@ -0,0 +1,18 @@ +package severity + +import ( + "github.com/lib/pq" + "gorm.io/gorm" +) + +type SeverityEntity struct { + gorm.Model + Name string `gorm:"column:name"` + Description string `gorm:"column:description"` + Sla int `gorm:"column:sla"` + SlackUserIds pq.StringArray `gorm:"column:slack_user_ids;type:string[]"` +} + +func (SeverityEntity) TableName() string { + return "severity" +} diff --git a/pkg/postgres/service/severity/severity.go b/pkg/postgres/service/severity/severity.go new file mode 100644 index 0000000..32905cf --- /dev/null +++ b/pkg/postgres/service/severity/severity.go @@ -0,0 +1,62 @@ +package severity + +import ( + "go.uber.org/zap" + "gorm.io/gorm" +) + +type Service struct { + logger *zap.Logger + gormClient *gorm.DB +} + +func NewSeverityService(logger *zap.Logger, gormClient *gorm.DB) *Service { + return &Service{ + logger: logger, + gormClient: gormClient, + } +} + +func (s *Service) GetAllActiveSeverity() (*[]SeverityEntity, error) { + var severityEntity []SeverityEntity + result := s.gormClient.Where("deleted_at is NULL").Find(&severityEntity) + if result.Error != nil { + s.logger.Error("fetching severity query failed", zap.Error(result.Error)) + return nil, result.Error + } + if result.RowsAffected == 0 { + return nil, nil + } + + return &severityEntity, nil +} + +func (s *Service) FindIncidentSeverityEntityById(id int) (*SeverityEntity, error) { + var severityEntity SeverityEntity + + result := s.gormClient.Find(&severityEntity, "id = ?", id) + if result.Error != nil { + s.logger.Error("fetching severity query failed", zap.Error(result.Error)) + return nil, result.Error + } else if result.RowsAffected == 0 { + s.logger.Error("SeverityEntity not found", zap.Error(result.Error)) + return nil, nil + } + + return &severityEntity, nil +} + +func (s *Service) FindSeverityById(severityId uint) (*SeverityEntity, error) { + var severityEntity SeverityEntity + + result := s.gormClient.Find(&severityEntity, "id = ?", severityId) + if result.Error != nil { + return nil, result.Error + } + + if result.RowsAffected == 0 { + return nil, nil + } + + return &severityEntity, nil +} diff --git a/pkg/postgres/service/tag/entity.go b/pkg/postgres/service/tag/entity.go new file mode 100644 index 0000000..294dcec --- /dev/null +++ b/pkg/postgres/service/tag/entity.go @@ -0,0 +1,34 @@ +package tag + +import "gorm.io/gorm" + +type Type string + +const ( + SingleValue Type = "single_value" + MultiValue = "multi_value" + FreeText = "free_text" +) + +type TagEntity struct { + gorm.Model + Name string `gorm:"column:name"` + Label string `gorm:"column:label"` + PlaceHolder string `gorm:"column:place_holder"` + ActionId string `gorm:"column:action_id"` + Type Type `gorm:"column:type"` +} + +func (TagEntity) TableName() string { + return "tag" +} + +type TagValueEntity struct { + gorm.Model + TagId uint `gorm:"column:tag_id"` + Value string `gorm:"column:value"` +} + +func (TagValueEntity) TableName() string { + return "tag_value" +} diff --git a/pkg/postgres/service/tag/model.go b/pkg/postgres/service/tag/model.go new file mode 100644 index 0000000..e3b2d05 --- /dev/null +++ b/pkg/postgres/service/tag/model.go @@ -0,0 +1,11 @@ +package tag + +type TagDTO struct { + Id uint + Name string + Type Type + Label string + PlaceHolder string + ActionId string + Optional bool +} diff --git a/pkg/postgres/service/tag/tag.go b/pkg/postgres/service/tag/tag.go new file mode 100644 index 0000000..41a9aa4 --- /dev/null +++ b/pkg/postgres/service/tag/tag.go @@ -0,0 +1,71 @@ +package tag + +import ( + "go.uber.org/zap" + "gorm.io/gorm" +) + +type Service struct { + logger *zap.Logger + gormClient *gorm.DB +} + +func NewTagService(logger *zap.Logger, gormClient *gorm.DB) *Service { + return &Service{ + logger: logger, + gormClient: gormClient, + } +} + +func (t *Service) FindById(id uint) (*TagEntity, error) { + tag := TagEntity{} + tx := t.gormClient.Raw("select * from tag where id = ?", id).Scan(&tag) + + if tx.Error != nil { + return nil, tx.Error + } + if tx.RowsAffected == 0 { + return nil, nil + } + + return &tag, nil +} + +func (t *Service) FindTagValuesByTagId(id uint) (*[]TagValueEntity, error) { + var tagValues []TagValueEntity + tx := t.gormClient.Raw("select tv.* from tag_value tv inner join tag t on t.id = tv.tag_id where t.id = ?", id).Scan(&tagValues) + + if tx.Error != nil { + return nil, tx.Error + } + if tx.RowsAffected == 0 { + return nil, nil + } + return &tagValues, nil +} + +func (t *Service) FindTagValuesByIds(ids []int32) (*[]TagValueEntity, error) { + var tagValues []TagValueEntity + tx := t.gormClient.Raw("select * from tag_value where id in ?", ids).Scan(&tagValues) + + if tx.Error != nil { + return nil, tx.Error + } + if tx.RowsAffected == 0 { + return nil, nil + } + return &tagValues, nil +} + +func (t *Service) FindTagsByTeamId(teamId uint) (*[]TagDTO, error) { + var tags []TagDTO + tx := t.gormClient.Raw("select t.*, tt.optional from tag t inner join team_tag tt on t.id = tt.tag_id where tt.team_id = ?", teamId).Scan(&tags) + + if tx.Error != nil { + return nil, tx.Error + } + if tx.RowsAffected == 0 { + return nil, nil + } + return &tags, nil +} diff --git a/pkg/postgres/service/team/entity.go b/pkg/postgres/service/team/entity.go new file mode 100644 index 0000000..14cef70 --- /dev/null +++ b/pkg/postgres/service/team/entity.go @@ -0,0 +1,28 @@ +package team + +import ( + "github.com/lib/pq" + "gorm.io/gorm" +) + +type TeamEntity struct { + gorm.Model + Name string `gorm:"column:name"` + SlackUserIds pq.StringArray `gorm:"column:slack_user_ids;type:string[]"` + Active bool `gorm:"column:active"` +} + +func (TeamEntity) TableName() string { + return "team" +} + +type TeamTagEntity struct { + gorm.Model + TeamId int `gorm:"column:team_id"` + TagId int `gorm:"column:tag_id"` + Optional bool `gorm:"column:optional"` +} + +func (TeamTagEntity) TableName() string { + return "team_tag" +} diff --git a/pkg/postgres/service/team/team.go b/pkg/postgres/service/team/team.go new file mode 100644 index 0000000..0149263 --- /dev/null +++ b/pkg/postgres/service/team/team.go @@ -0,0 +1,43 @@ +package team + +import ( + "go.uber.org/zap" + "gorm.io/gorm" +) + +type Service struct { + logger *zap.Logger + gormClient *gorm.DB +} + +func NewTeamService(logger *zap.Logger, gormClient *gorm.DB) *Service { + return &Service{ + logger: logger, + gormClient: gormClient, + } +} + +func (s *Service) GetAllActiveTeams() (*[]TeamEntity, error) { + var teamEntity []TeamEntity + + result := s.gormClient.Find(&teamEntity, "active = ?", true) + if result.Error != nil { + return nil, result.Error + } else if result.RowsAffected == 0 { + return nil, nil + } + return &teamEntity, nil +} + +func (s *Service) FindTeamById(teamId uint) (*TeamEntity, error) { + var teamEntity TeamEntity + + result := s.gormClient.Find(&teamEntity, "id = ?", teamId) + if result.Error != nil { + return nil, result.Error + } else if result.RowsAffected == 0 { + return nil, nil + } + + return &teamEntity, nil +} diff --git a/pkg/postgres/service/user/model.go b/pkg/postgres/service/user/model.go new file mode 100644 index 0000000..f91a62e --- /dev/null +++ b/pkg/postgres/service/user/model.go @@ -0,0 +1,14 @@ +package user + +import "gorm.io/gorm" + +type UserEntity struct { + gorm.Model + Name string + SlackUserId string + Active bool +} + +func (UserEntity) TableName() string { + return "houston_user" +} diff --git a/pkg/postgres/service/user/user.go b/pkg/postgres/service/user/user.go new file mode 100644 index 0000000..a00006b --- /dev/null +++ b/pkg/postgres/service/user/user.go @@ -0,0 +1 @@ +package user diff --git a/pkg/slack/common/channel.go b/pkg/slack/common/channel.go deleted file mode 100644 index cca95ca..0000000 --- a/pkg/slack/common/channel.go +++ /dev/null @@ -1,49 +0,0 @@ -package common - -import ( - "fmt" - - "github.com/slack-go/slack" - "github.com/slack-go/slack/socketmode" - "go.uber.org/zap" -) - -func FindParticipants(client *socketmode.Client, logger *zap.Logger, channelId string) ([]string, error) { - request := &slack.GetUsersInConversationParameters{ - ChannelID: channelId, - Limit: 1000, - } - channelInfo, _, err := client.GetUsersInConversation(request) - if err != nil { - return nil, fmt.Errorf("fetch channel conversationInfo failed. err: %v", err) - } - - return channelInfo, nil -} - -func CreateChannel(client *socketmode.Client, logger *zap.Logger, channelName string) (string, error) { - request := slack.CreateConversationParams{ - ChannelName: channelName, - IsPrivate: false, - } - - channel, err := client.CreateConversation(request) - if err != nil { - logger.Error("create slack channel failed", zap.String("channel_name", channelName), zap.Error(err)) - return "", err - } - - logger.Info("created slack channel successfully", zap.String("channel_name", channelName), zap.String("channel_id", channel.ID)) - return channel.ID, nil -} - -func InviteUsersToConversation(client *socketmode.Client, logger *zap.Logger, channelId string, userId ...string) { - _, err := client.InviteUsersToConversation(channelId, userId...) - if err != nil { - logger.Error("invite users to conversation failed", - zap.String("channel_id", channelId), zap.Any("user_ids", userId), zap.Error(err)) - return - } - - logger.Info("successfully invite users to conversation", zap.String("channel_id", channelId), zap.Any("user_ids", userId)) -} diff --git a/pkg/slack/config.go b/pkg/slack/config.go deleted file mode 100644 index 578351f..0000000 --- a/pkg/slack/config.go +++ /dev/null @@ -1 +0,0 @@ -package slack diff --git a/pkg/slack/houston/blazeless_main_command.go b/pkg/slack/houston/blazeless_main_command.go deleted file mode 100644 index a5658cb..0000000 --- a/pkg/slack/houston/blazeless_main_command.go +++ /dev/null @@ -1,38 +0,0 @@ -package houston - -import ( - "houston/pkg/postgres/query" - houston "houston/pkg/slack/houston/design" - - "github.com/slack-go/slack" - "github.com/slack-go/slack/socketmode" - "go.uber.org/zap" - "gorm.io/gorm" -) - -func HoustonMainCommand(db *gorm.DB, client *socketmode.Client, logger *zap.Logger, 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 - } - - //TODO - DOES NOT THROW ERROR IF SAME SLACK ID IS PRESENT MULTIPLE TIMES - result, err := query.FindIncidentByChannelId(db, cmd.ChannelID) - if err != nil { - logger.Error("FindIncidentBySlackChannelId errors", - zap.String("channel_id", cmd.ChannelID), zap.String("channel", cmd.ChannelName), - zap.String("user_id", cmd.UserID), zap.Error(err)) - return - } - - if result != nil { - logger.Info("Result", zap.String("result", result.IncidentName)) - payload := houston.OptionsBlock() - client.Ack(*evt.Request, payload) - } - - payload := houston.ButtonDesign() - client.Ack(*evt.Request, payload) -} diff --git a/pkg/slack/houston/command/incident_show_tags.go b/pkg/slack/houston/command/incident_show_tags.go deleted file mode 100644 index dce07e6..0000000 --- a/pkg/slack/houston/command/incident_show_tags.go +++ /dev/null @@ -1,108 +0,0 @@ -package command - -import ( - "fmt" - "houston/pkg/postgres/query" - - "github.com/slack-go/slack" - "github.com/slack-go/slack/socketmode" - "go.uber.org/zap" - "gorm.io/gorm" -) - -type incidenShowTagsProcessor struct { - client *socketmode.Client - db *gorm.DB - logger *zap.Logger -} - -func NewIncidentShowTagsProcessor(client *socketmode.Client, db *gorm.DB, logger *zap.Logger) *incidenShowTagsProcessor { - return &incidenShowTagsProcessor{ - client: client, - db: db, - logger: logger, - } -} - -func (isp *incidenShowTagsProcessor) IncidentShowTagsRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { - result, err := query.FindIncidentSeverityTeamJoin(isp.db, callback.Channel.ID) - if err != nil { - isp.logger.Error("IncidentShowTagsRequestProcess error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } else if result == nil { - isp.logger.Error("IncidentSeverityTeamJoin Not found", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - - savedContributingFactor, err := query.FindContributingFactorsByIncidentId(isp.db, result.IncidentId) - if err != nil { - isp.logger.Error("FindContributingFactorsByIncidentId error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - cfString := "No tags" - if savedContributingFactor != nil { - cfString = savedContributingFactor.Label - } - - savedCustomerTags, err := query.FindCustomerTagsByIncidentId(isp.logger, isp.db, result.IncidentId) - if err != nil { - isp.logger.Error("FindCustomerTagsByIncidentId error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - - var customerTagString string - if len(savedCustomerTags) == 0 { - customerTagString = "No tags" - } else { - for _, o := range savedCustomerTags { - customerTagString = customerTagString + " " + o.Label - } - } - - savedTags, err := query.FindTagsByIncidentId(isp.db, result.IncidentId) - if err != nil { - isp.logger.Error("FindTagsByIncidentId error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - - var tagString string - if len(savedTags) == 0 { - tagString = "No tags" - } else { - for _, o := range savedTags { - tagString = tagString + " " + o.Label - } - } - - savedDp, err := query.FindDataPlataformTagsByIncidentId(result.IncidentId, isp.db, isp.logger) - if err != nil { - isp.logger.Error("FindDataPlataformTagsByIncidentId error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - var dpString string = "No tags" - if savedDp != nil && (len(savedDp.DataPlatformTag)) != 0 { - dpString = savedDp.DataPlatformTag - } - - msgOption := slack.MsgOptionText(fmt.Sprintf("\n\nCONTRIBUTING-FACTORS: \n %s \n\n CUSTOMER: \n %s \n\n CUSTOMER:%s \n %s \n\n DATA-PLATFORM: \n %s", cfString, customerTagString, result.TeamsName, tagString, dpString), true) - _, errMessage := isp.client.PostEphemeral(callback.Channel.ID, callback.User.ID, msgOption) - if errMessage != nil { - isp.logger.Error("PostEphemeralmresponse failed for IncidentShowTagsRequestProcess", zap.Error(errMessage)) - return - } - var payload interface{} - isp.client.Ack(*request, payload) - -} diff --git a/pkg/slack/houston/command/incident_update_tags.go b/pkg/slack/houston/command/incident_update_tags.go deleted file mode 100644 index 99b01a1..0000000 --- a/pkg/slack/houston/command/incident_update_tags.go +++ /dev/null @@ -1,315 +0,0 @@ -package command - -import ( - "houston/entity" - "houston/pkg/postgres/query" - houston "houston/pkg/slack/houston/design" - "strconv" - - "github.com/slack-go/slack" - "github.com/slack-go/slack/socketmode" - "go.uber.org/zap" - "gorm.io/gorm" -) - -type incidentUpdateTagsProcessor struct { - client *socketmode.Client - db *gorm.DB - logger *zap.Logger -} - -func NewIncidentUpdateTagsProcessor(client *socketmode.Client, db *gorm.DB, logger *zap.Logger) *incidentUpdateTagsProcessor { - return &incidentUpdateTagsProcessor{ - client: client, - db: db, - logger: logger, - } -} - -func (idp *incidentUpdateTagsProcessor) IncidentUpdateTagsRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { - result, err := query.FindIncidentSeverityTeamJoin(idp.db, callback.Channel.ID) - if err != nil { - idp.logger.Error("FindIncidentSeverityTeamJoin error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } else if result == nil { - idp.logger.Error("IncidentSeverityTeamJoin Not found", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - tags, err := query.FindTagsByTeamsId(idp.db, result.TeamsId) - if err != nil { - idp.logger.Error("FindTagsByTeamsId error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - incidentTags, err := query.FindTagsByIncidentId(idp.db, result.IncidentId) - if err != nil { - idp.logger.Error("FindTagsByIncidentId error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - - cf, savedCf, err := findContributingFactors(result.IncidentId, idp.db, idp.logger) - if err != nil { - idp.logger.Error("findContrxibutingFactors error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - - customerTags, savedCustomrTags, err := findCustomerTags(result.IncidentId, idp.db, idp.logger) - if err != nil { - idp.logger.Error("findCustomerTags error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - - savedDataPlatformTags, err := query.FindDataPlataformTagsByIncidentId(result.IncidentId, idp.db, idp.logger) - if err != nil { - idp.logger.Error("findDataPlataformTagsByIncidentInt error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - - modalRequest := houston.BuildIncidentUpdateTagModal(callback.Channel, tags, result.TeamsName, incidentTags, savedCf, cf, savedCustomrTags, customerTags, savedDataPlatformTags) - - _, err = idp.client.OpenView(callback.TriggerID, modalRequest) - if err != nil { - idp.logger.Error("houston slack openview command for IncidentUpdateTagsRequestProcess failed.", - zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) - return - } - var payload interface{} - idp.client.Ack(*request, payload) -} - -func (itp *incidentUpdateTagsProcessor) IncidentUpdateTags(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { - result, err := query.FindIncidentSeverityTeamJoin(itp.db, callback.View.PrivateMetadata) - if err != nil { - itp.logger.Error("FindIncidentSeverityTeamJoin error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } else if result == nil { - itp.logger.Error("IncidentSeverityTeamJoin Not found", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - cfId, customerTagIdRequests, incidentTagIdRequests, dataPlatformString := buildUpdateIncidentTagsRequest(itp.logger, callback.View.State.Values) - - //Update Contributing factors - if cfId == 0 { - err = query.SetContributingFactorDeletedAt(itp.db, result.IncidentId) - if err != nil { - itp.logger.Error("SetContributingFactorDeletedAt err", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - - } else { - err = query.UpsertContributingFactorIdToIncidentId(itp.db, cfId, result.IncidentId) - if err != nil { - itp.logger.Error("UpsertContributingFactorIdToIncidentId err", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - } - - // Update Customer Tag Id mapping - customerTagIdExist, err := query.FindCustomerTagIdMappingWithIncidentByIncidentId(itp.db, result.IncidentId) - var customerTagIdList []int - - for _, o := range customerTagIdExist { - customerTagIdList = append(customerTagIdList, o.CustomerTagsId) - } - - UpdateCustomerTagId(result, callback, itp.db, itp.logger, customerTagIdList, customerTagIdRequests) - - // Team tag Id Mapping - tagsExist, err := query.FindTagRecordListByIncidentId(itp.db, result.IncidentId) - var tagIdList []int - for _, o := range tagsExist { - tagIdList = append(tagIdList, o.TagId) - } - - UpdateTeamTagId(result, callback, itp.db, itp.logger, tagIdList, incidentTagIdRequests) - - // Update Data Platform tags - err = query.UpsertDataPlatformTagToIncidentId(itp.db, dataPlatformString, result.IncidentId) - if err != nil { - itp.logger.Error("UpsertDataPlatformTagToIncidentId err", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - var payload interface{} - itp.client.Ack(*request, payload) -} - -func buildUpdateIncidentTagsRequest(logger *zap.Logger, blockActions map[string]map[string]slack.BlockAction) (int, []int, []int, string) { - var requestMap = make(map[string][]slack.OptionBlockObject) - var requestMapString = make(map[string]string, 0) - for _, actions := range blockActions { - for actionID, action := range actions { - if action.Type == "multi_static_select" { - requestMap[actionID] = action.SelectedOptions - } else if action.Type == "static_select" { - requestMapString[actionID] = action.SelectedOption.Value - } else if action.Type == "plain_text_input" { - requestMapString[actionID] = action.Value - } - } - } - var customerTags []int - var tagsList []int - - for _, o := range requestMap["incident_customer_tags_modal_request"] { - selectedValueInInt, err := strconv.Atoi(o.Value) - if err != nil { - logger.Error("String to int conversion failed in buildUpdateIncidentTagseRequest for " + o.Value) - } else { - customerTags = append(customerTags, selectedValueInInt) - } - } - - for _, o := range requestMap["incident_tags_modal_request"] { - selectedValueInInt, err := strconv.Atoi(o.Value) - if err != nil { - logger.Error("String to int conversion failed in buildUpdateIncidentTagseRequest for " + o.Value) - } else { - tagsList = append(tagsList, selectedValueInInt) - } - } - - var cfId int - var err1 error - if len(requestMapString["incident_cf_modal_request"]) != 0 { - cfId, err1 = strconv.Atoi(requestMapString["incident_cf_modal_request"]) - if err1 != nil { - logger.Error("String to int conversion in CF failed in buildUpdateIncidentTagseRequest for " + requestMapString["incident_cf_modal_request"]) - } - } - - return cfId, customerTags, tagsList, requestMapString["incident_data_platform_tag_modal_request"] -} - -func diffLists(list1 []int, list2 []int) ([]int, []int) { - set1 := make(map[int]bool) - set2 := make(map[int]bool) - result1 := []int{} - result2 := []int{} - - for _, num := range list1 { - set1[num] = true - } - - for _, num := range list2 { - set2[num] = true - } - - for num, _ := range set1 { - if _, ok := set2[num]; !ok { - result1 = append(result1, num) - } - } - - for num, _ := range set2 { - if _, ok := set1[num]; !ok { - result2 = append(result2, num) - } - } - - return result1, result2 -} - -func findContributingFactors(incidentId int, db *gorm.DB, logger *zap.Logger) ([]entity.ContributingFactorEntity, *entity.ContributingFactorEntity, error) { - cf, err := query.FetchAllContributingFactors(db) - if err != nil { - logger.Error("FetchAllContributingFactors error") - return nil, nil, err - } - savedCf, err := query.FindContributingFactorsByIncidentId(db, incidentId) - if err != nil { - logger.Error("FindContributingFactorsByIncidentId error") - return nil, nil, err - } - return cf, savedCf, nil - -} - -func findCustomerTags(incidentId int, db *gorm.DB, logger *zap.Logger) ([]entity.CustomerTagsEntity, []entity.CustomerTagsEntity, error) { - customerTags, err := query.FetchAllCustomerTags(db) - if err != nil { - logger.Error("FetchAllCustomerTags error") - return nil, nil, err - } - savedCustomerTags, err := query.FindCustomerTagsByIncidentId(logger, db, incidentId) - if err != nil { - logger.Error("FindCustomerTagsByIncidentId error") - return nil, nil, err - } - return customerTags, savedCustomerTags, nil - -} - -func UpdateCustomerTagId(result *entity.IncidentSeverityTeamJoinEntity, callback slack.InteractionCallback, db *gorm.DB, logger *zap.Logger, customerTagIdList []int, customerTagIdRequests []int) { - - listCustomerTag1, listCustomerTag2 := diffLists(customerTagIdList, customerTagIdRequests) - - for _, o := range listCustomerTag1 { - err := query.SetIncidentCustomerTagDeletedAt(db, o, result.IncidentId) - if err != nil { - logger.Error("SetIncidentCustomerTagDeletedAt error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - } - - for _, index := range listCustomerTag2 { - err := query.AddCustomerIdMappingToIncidentId(db, index, result.IncidentId) - if err != nil { - logger.Error("AddCustomerIMappingToIncidentId error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - } - -} - -func UpdateTeamTagId(result *entity.IncidentSeverityTeamJoinEntity, callback slack.InteractionCallback, db *gorm.DB, logger *zap.Logger, tagIdList []int, tagIdRequests []int) { - - list1, list2 := diffLists(tagIdList, tagIdRequests) - - for _, o := range list1 { - err := query.SetTagsDeletedAt(db, o, result.IncidentId) - if err != nil { - logger.Error("SetDeletedAt error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - } - - for _, index := range list2 { - err := query.AddTagsIdMappingToIncidentId(db, index, result.IncidentId) - if err != nil { - logger.Error("AddTagsIdMappingToIncidentId error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - } - -} diff --git a/pkg/slack/houston/command/incident_update_type.go b/pkg/slack/houston/command/incident_update_type.go deleted file mode 100644 index 567f55b..0000000 --- a/pkg/slack/houston/command/incident_update_type.go +++ /dev/null @@ -1,123 +0,0 @@ -package command - -import ( - "fmt" - "houston/pkg/postgres/query" - "houston/pkg/slack/common" - houston "houston/pkg/slack/houston/design" - "strconv" - - "github.com/slack-go/slack" - "github.com/slack-go/slack/socketmode" - "go.uber.org/zap" - "gorm.io/gorm" -) - -type incidentUpdateTypeProcessor struct { - client *socketmode.Client - db *gorm.DB - logger *zap.Logger -} - -func NewIncidentUpdateTypeProcessor(client *socketmode.Client, db *gorm.DB, logger *zap.Logger) *incidentUpdateTypeProcessor { - return &incidentUpdateTypeProcessor{ - client: client, - db: db, - logger: logger, - } -} - -func (itp *incidentUpdateTypeProcessor) IncidentUpdateTypeRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { - teams, err := query.FindTeamList(itp.db) - if err != nil { - itp.logger.Error("FindTeamList error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - - modalRequest := houston.BuildIncidentUpdateTypeModal(callback.Channel, teams) - - _, err = itp.client.OpenView(callback.TriggerID, modalRequest) - if err != nil { - itp.logger.Error("houston slack openview command failed.", - zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) - return - } - var payload interface{} - itp.client.Ack(*request, payload) -} - -func (isp *incidentUpdateTypeProcessor) IncidentUpdateType(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { - incidentEntity, err := query.FindIncidentByChannelId(isp.db, callback.View.PrivateMetadata) - if err != nil { - isp.logger.Error("FindIncidentByChannelId error", - zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), - zap.String("user_id", user.ID), zap.Error(err)) - return - } else if incidentEntity == nil { - isp.logger.Error("IncidentEntity not found ", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - - incidentTypeId := buildUpdateIncidentTypeRequest(isp.logger, callback.View.State.Values) - result, err := query.FindTeamById(isp.db, incidentTypeId) - if err != nil { - isp.logger.Error("FindTeamEntityById error", - zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), - zap.String("user_id", user.ID), zap.Error(err)) - return - } else if result == nil { - isp.logger.Error("Team Not Found", - zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), - zap.String("user_id", user.ID), zap.Error(err)) - return - } - incidentEntity.TeamsId = int(result.ID) - incidentEntity.UpdatedBy = user.ID - err = query.UpdateIncident(isp.db, incidentEntity) - if err != nil { - isp.logger.Error("UpdateIncident error", - zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), - zap.String("user_id", user.ID), zap.Error(err)) - } - - userIdList, err := query.FindDefaultUserIdToBeAddedByTeam(isp.db, int(result.ID)) - if err != nil { - isp.logger.Error("FindDefaultUserIdToBeAddedByTeam error", - zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), - zap.String("user_id", user.ID), zap.Error(err)) - return - } - for _, o := range userIdList { - common.InviteUsersToConversation(isp.client, isp.logger, callback.View.PrivateMetadata, o) - } - msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> > set Team to %s", user.ID, result.Name), false) - _, _, errMessage := isp.client.PostMessage(callback.View.PrivateMetadata, msgOption) - if errMessage != nil { - isp.logger.Error("post response failed for IncidentUpdateType", zap.Error(errMessage)) - return - } - var payload interface{} - isp.client.Ack(*request, payload) -} - -func buildUpdateIncidentTypeRequest(logger *zap.Logger, blockActions map[string]map[string]slack.BlockAction) int { - var requestMap = make(map[string]string, 0) - for _, actions := range blockActions { - for actionID, action := range actions { - if action.Type == "static_select" { - requestMap[actionID] = action.SelectedOption.Value - } - } - } - - selectedValue := requestMap["incident_type_modal_request"] - selectedValueInInt, err := strconv.Atoi(selectedValue) - if err != nil { - logger.Error("String conversion to int faileed in buildUpdateIncidentTypeRequest for "+selectedValue, zap.Error(err)) - } - return selectedValueInInt -} diff --git a/pkg/slack/houston/command/member_join_event.go b/pkg/slack/houston/command/member_join_event.go deleted file mode 100644 index c7487b7..0000000 --- a/pkg/slack/houston/command/member_join_event.go +++ /dev/null @@ -1,56 +0,0 @@ -package command - -import ( - "houston/pkg/postgres/query" - houston "houston/pkg/slack/houston/design" - - "github.com/slack-go/slack" - "github.com/slack-go/slack/slackevents" - "github.com/slack-go/slack/socketmode" - "go.uber.org/zap" - "gorm.io/gorm" -) - -type memberJoinProcessor struct { - client *socketmode.Client - db *gorm.DB - logger *zap.Logger -} - -func NewMemberJoinProcessor(socketmodeClient *socketmode.Client, db *gorm.DB, logger *zap.Logger) *memberJoinProcessor { - return &memberJoinProcessor{ - client: socketmodeClient, - db: db, - logger: logger, - } -} - -func (mp *memberJoinProcessor) MemberJoinProcessCommand(memberJoinedChannelEvent *slackevents.MemberJoinedChannelEvent) { - mp.logger.Info("processing member join event", zap.String("channel", memberJoinedChannelEvent.Channel)) - - incidentEntity, err := query.FindIncidentByChannelId(mp.db, memberJoinedChannelEvent.Channel) - if err != nil { - mp.logger.Error("error in searching incident", zap.String("channel", memberJoinedChannelEvent.Channel), - zap.String("user_id", memberJoinedChannelEvent.User), zap.Error(err)) - return - } else if err == nil && incidentEntity == nil { - mp.logger.Info("incident not found", zap.String("channel", memberJoinedChannelEvent.Channel), - zap.String("user_id", memberJoinedChannelEvent.User), zap.Error(err)) - return - } - - blocks, err := houston.IncidentSummarySection(incidentEntity, mp.db) - if err != nil { - mp.logger.Error("error in creating incident summary section inside incident", - zap.String("channel", memberJoinedChannelEvent.Channel), - zap.String("user_id", memberJoinedChannelEvent.User), zap.Error(err)) - return - } - mp.logger.Info("member join block", zap.Any("blocks", blocks)) - color := query.GetColorBySeverity(incidentEntity.SeverityId) - msgOption := slack.MsgOptionAttachments(slack.Attachment{Blocks: blocks, Color: color}) - _, err = mp.client.PostEphemeral(memberJoinedChannelEvent.Channel, memberJoinedChannelEvent.User, msgOption) - if err != nil { - mp.logger.Error("post response failed", zap.Error(err)) - } -} diff --git a/pkg/slack/houston/command/show_incidents.go b/pkg/slack/houston/command/show_incidents.go deleted file mode 100644 index 291c30b..0000000 --- a/pkg/slack/houston/command/show_incidents.go +++ /dev/null @@ -1,49 +0,0 @@ -package command - -import ( - "houston/pkg/postgres/query" - houston "houston/pkg/slack/houston/design" - - "github.com/slack-go/slack" - "github.com/slack-go/slack/socketmode" - "github.com/spf13/viper" - "go.uber.org/zap" - "gorm.io/gorm" -) - -type ShowIncidentsButtonProcessor struct { - client *socketmode.Client - db *gorm.DB - logger *zap.Logger -} - -func ShowIncidentsProcessor(client *socketmode.Client, db *gorm.DB, logger *zap.Logger) *ShowIncidentsButtonProcessor { - return &ShowIncidentsButtonProcessor{ - client: client, - db: db, - logger: logger, - } -} - -func (sip *ShowIncidentsButtonProcessor) ProcessShowIncidentsButtonCommand(channel slack.Channel, user slack.User, triggerId string, request *socketmode.Request) { - - limit := viper.GetInt("SHOW_INCIDENTS_LIMT") - s, err := query.FindNotResolvedLatestIncidents(sip.db, limit) - - if err != nil { - sip.logger.Error("FindNotResolvedLatestIncidents query failed.", - zap.String("trigger_id", triggerId), zap.String("channel_id", channel.ID), zap.Error(err)) - return - } - blocks := houston.GenerateModalForShowIncidentsButtonSection(s) - msgOption := slack.MsgOptionBlocks(blocks...) - _, err = sip.client.Client.PostEphemeral(channel.ID, user.ID, msgOption) - if err != nil { - sip.logger.Error("houston slack PostEphemeral command failed for ProcessShowIncidentsButtonCommand.", - zap.String("trigger_id", triggerId), zap.String("channel_id", channel.ID), zap.String("user_id", user.ID), zap.Error(err)) - return - } - var payload interface{} - sip.client.Ack(*request, payload) - -} diff --git a/pkg/slack/houston/command/start_incident.go b/pkg/slack/houston/command/start_incident.go deleted file mode 100644 index 63019c2..0000000 --- a/pkg/slack/houston/command/start_incident.go +++ /dev/null @@ -1,38 +0,0 @@ -package command - -import ( - houston "houston/pkg/slack/houston/design" - - "github.com/slack-go/slack/socketmode" - "go.uber.org/zap" - "gorm.io/gorm" -) - -type startIncidentButtonProcessor struct { - client *socketmode.Client - db *gorm.DB - logger *zap.Logger -} - -func NewStartIncidentProcessor(client *socketmode.Client, db *gorm.DB, logger *zap.Logger) *startIncidentButtonProcessor { - return &startIncidentButtonProcessor{ - client: client, - db: db, - logger: logger, - } -} - -func (sip *startIncidentButtonProcessor) ProcessStartIncidentButtonCommand( - client *socketmode.Client, request *socketmode.Request, channelId, triggerId string) { - modal := houston.GenerateModalRequest(sip.db, sip.logger, channelId) - _, err := sip.client.OpenView(triggerId, modal) - if err != nil { - sip.logger.Error("houston slack openview command failed.", - zap.String("trigger_id", triggerId), zap.String("channel_id", channelId), zap.Error(err)) - return - } - - sip.logger.Info("houston successfully send modal to slack", zap.String("trigger_id", triggerId)) - var payload interface{} - client.Ack(*request, payload) -} diff --git a/pkg/slack/houston/command/start_incident_modal_submission.go b/pkg/slack/houston/command/start_incident_modal_submission.go deleted file mode 100644 index 6038d22..0000000 --- a/pkg/slack/houston/command/start_incident_modal_submission.go +++ /dev/null @@ -1,244 +0,0 @@ -package command - -import ( - "context" - "encoding/json" - "fmt" - "houston/entity" - "houston/model" - "houston/model/request" - "houston/pkg/postgres/query" - "houston/pkg/slack/common" - houston "houston/pkg/slack/houston/design" - "strconv" - "time" - - "github.com/google/uuid" - "github.com/slack-go/slack" - "github.com/slack-go/slack/socketmode" - "go.uber.org/zap" - "google.golang.org/api/calendar/v3" - "google.golang.org/api/option" - "gorm.io/gorm" -) - -type createIncidentProcessor struct { - client *socketmode.Client - logger *zap.Logger - db *gorm.DB -} - -func NewCreateIncidentProcessor(client *socketmode.Client, logger *zap.Logger, db *gorm.DB) *createIncidentProcessor { - return &createIncidentProcessor{ - client: client, - logger: logger, - db: db, - } -} - -func (cip *createIncidentProcessor) CreateIncidentModalCommandProcessing(callback slack.InteractionCallback, request *socketmode.Request) { - // Build create incident request - createIncidentRequest := buildCreateIncidentRequest(callback.View.State.Values) - createIncidentRequest.RequestOriginatedSlackChannel = callback.View.PrivateMetadata - cip.logger.Info("incident request created", zap.Any("request", createIncidentRequest)) - - // Create a new Slack channel for the incident - ai := query.AutoIncrementID{} - id, err := ai.Next(cip.db) - if err != nil || id == 0 { - cip.logger.Error("unable to generate id", zap.Error(err)) - return - } - channelName := fmt.Sprintf("houston-%s", strconv.Itoa(id)) - channelID, err := common.CreateChannel(cip.client, cip.logger, channelName) - if err != nil { - cip.logger.Error("Unable to create channel", zap.Error(err)) - return - } - createIncidentRequest.Status = entity.Investigating - createIncidentRequest.IncidentName = channelName - createIncidentRequest.SlackChannel = channelID - - // Set default values for some fields - teamId, _ := strconv.Atoi(createIncidentRequest.IncidentType) - createIncidentRequest.DetectionTime = nil - createIncidentRequest.CustomerImpactStartTime = time.Now() - createIncidentRequest.CustomerImpactEndTime = nil - createIncidentRequest.TeamsId = teamId - createIncidentRequest.JiraId = "" - createIncidentRequest.ConfluenceId = "" - createIncidentRequest.RemindMeAt = nil - createIncidentRequest.EnableReminder = false - createIncidentRequest.CreatedBy = callback.User.ID - createIncidentRequest.UpdatedBy = callback.User.ID - createIncidentRequest.Version = 0 - - // Save the incident to the database - incidentEntity, err := query.CreateIncident(cip.db, createIncidentRequest) - if err != nil { - cip.logger.Error("Error while creating incident", zap.Error(err)) - return - } - - // Post incident summary to Blaze Group channel and incident channel - err = postIncidentSummary(cip.client, callback.View.PrivateMetadata, channelID, incidentEntity, cip.db) - if err != nil { - cip.logger.Error("error while posting incident summary", zap.Error(err)) - } - err = addDefaultUsersToIncident(cip, channelID, incidentEntity.SeverityId, incidentEntity.TeamsId) - if err != nil { - cip.logger.Error("error while adding default users to incident", zap.Error(err)) - return - } - - tagOncallToIncident(cip, teamId, channelID) - - //gmeet := createGmeetLink(cip.logger, channelName) - //if len(gmeet) != 0 { - // msgOption := slack.MsgOptionText(fmt.Sprintf("gmeet Link :", gmeet), false) - // cip.client.PostMessage(channelID, msgOption) - //} - - // Acknowledge the interaction callback - var payload interface{} - cip.client.Ack(*request, payload) -} - -func tagOncallToIncident(cip *createIncidentProcessor, teamId int, channelId string) { - teamEntity, err := query.FindTeamById(cip.db, teamId) - if (err != nil) { - cip.logger.Error("error in fetching team err: %v", zap.Error(err)) - } - msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s>", teamEntity.OncallHandle), false) - _, ts, _ := cip.client.PostMessage(channelId, msgOption) - InviteOncallPersonToIncident(cip, channelId, ts) -} - -func InviteOncallPersonToIncident(cip *createIncidentProcessor, channelId, ts string) { - go func() { - time.Sleep(3 * time.Second) - msg, _, _, _ := cip.client.GetConversationReplies(&slack.GetConversationRepliesParameters{ - ChannelID: channelId, - Timestamp: ts, - Limit: 2, - }, - ) - if (len(msg) > 1) { - //User id needs to sliced from `<@XXXXXXXXXXXX>` format to `XXXXXXXXXXXX` - cip.client.InviteUsersToConversation(channelId, msg[1].Text[2:13]) - } - }() -} - -func addDefaultUsersToIncident(cip *createIncidentProcessor, channelId string, severityId, teamId int) (error) { - userIdList, err := query.FindDefaultUserIdToBeAddedBySeverity(cip.db, severityId) - if err != nil { - return err - } - for _, o := range userIdList { - common.InviteUsersToConversation(cip.client, cip.logger, channelId, o) - } - userIdList, err = query.FindDefaultUserIdToBeAddedByTeam(cip.db, teamId) - if err != nil { - return err - } - for _, o := range userIdList { - common.InviteUsersToConversation(cip.client, cip.logger, channelId, o) - } - return nil -} - -func postIncidentSummary( - client *socketmode.Client, - blazeGroupChannelID, incidentChannelID string, - incidentEntity *entity.IncidentEntity, - db *gorm.DB) error { - // Post incident summary to Blaze Group channel and incident channel - blocks, err := houston.IncidentSummarySection(incidentEntity, db) - if err != nil { - return fmt.Errorf("error in creating incident summary err: %v", err) - } - color := query.GetColorBySeverity(incidentEntity.SeverityId) - att := slack.Attachment{Blocks: blocks, Color: color} - _, timestamp, err := client.PostMessage(blazeGroupChannelID, slack.MsgOptionAttachments(att)) - if (err == nil) { - query.CreateMessage(db, &request.CreateMessage{ - SlackChannel: blazeGroupChannelID, - MessageTimeStamp: timestamp, - IncidentName: incidentEntity.IncidentName, - }) - } else { - return fmt.Errorf("error in saving message %v", err) - } - _, timestamp, err = client.PostMessage(incidentChannelID, slack.MsgOptionAttachments(att)) - if (err == nil) { - query.CreateMessage(db, &request.CreateMessage{ - SlackChannel: incidentChannelID, - MessageTimeStamp: timestamp, - IncidentName: incidentEntity.IncidentName, - }) - } else { - return fmt.Errorf("error in saving message %v", err) - } - return nil -} - -func buildCreateIncidentRequest(blockActions map[string]map[string]slack.BlockAction) *model.CreateIncident { - var createIncidentRequest model.CreateIncident - var requestMap = make(map[string]string, 0) - for _, actions := range blockActions { - for actionID, action := range actions { - if action.Type == "plain_text_input" { - requestMap[actionID] = action.Value - } - if action.Type == "static_select" { - requestMap[actionID] = action.SelectedOption.Value - } - } - } - - desRequestMap, _ := json.Marshal(requestMap) - json.Unmarshal(desRequestMap, &createIncidentRequest) - return &createIncidentRequest -} - -func createGmeetLink(logger *zap.Logger, channelName string) string { - calclient, err := calendar.NewService(context.Background(), option.WithCredentialsFile("")) - if err != nil { - logger.Error("Unable to read client secret file: ", zap.Error(err)) - return "" - } - t0 := time.Now().Format(time.RFC3339) - t1 := time.Now().Add(1 * time.Hour).Format(time.RFC3339) - event := &calendar.Event{ - Summary: channelName, - Description: "Incident", - Start: &calendar.EventDateTime{ - DateTime: t0, - }, - End: &calendar.EventDateTime{ - DateTime: t1, - }, - ConferenceData: &calendar.ConferenceData{ - CreateRequest: &calendar.CreateConferenceRequest{ - RequestId: uuid.NewString(), - ConferenceSolutionKey: &calendar.ConferenceSolutionKey{ - Type: "hangoutsMeet", - }, - Status: &calendar.ConferenceRequestStatus{ - StatusCode: "success", - }, - }, - }, - } - - calendarID := "primary" //use "primary" - event, err = calclient.Events.Insert(calendarID, event).ConferenceDataVersion(1).Do() - if err != nil { - logger.Error("Unable to create event. %v\n", zap.Error(err)) - return "" - } - - calclient.Events.Delete(calendarID, event.Id).Do() - return event.HangoutLink -} diff --git a/pkg/slack/houston/design/blazeless_modal.go b/pkg/slack/houston/design/blazeless_modal.go deleted file mode 100644 index df33258..0000000 --- a/pkg/slack/houston/design/blazeless_modal.go +++ /dev/null @@ -1,92 +0,0 @@ -package houston - -import ( - "houston/entity" - "houston/pkg/postgres/query" - "strconv" - - "github.com/slack-go/slack" - "go.uber.org/zap" - "gorm.io/gorm" -) - -func GenerateModalRequest(db *gorm.DB, logger *zap.Logger, channel string) slack.ModalViewRequest { - // Create a ModalViewRequest with a header and two inputs - titleText := slack.NewTextBlockObject("plain_text", "Houston", false, false) - closeText := slack.NewTextBlockObject("plain_text", "Close", false, false) - submitText := slack.NewTextBlockObject("plain_text", "Send", false, false) - - headerText := slack.NewTextBlockObject("mrkdwn", "Start Incident", false, false) - headerSection := slack.NewSectionBlock(headerText, nil, nil) - - teams, _ := query.FindTeam(db) - incidentTypeOptions := createOptionBlockObjectsForTeams(teams) - incidentTypeText := slack.NewTextBlockObject(slack.PlainTextType, "Incident Type", false, false) - incidentTypePlaceholder := slack.NewTextBlockObject("plain_text", "The incident type for the incident to create", false, false) - incidentTypeOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, incidentTypePlaceholder, "incident_type", incidentTypeOptions...) - incidentTypeBlock := slack.NewInputBlock("incident_type", incidentTypeText, nil, incidentTypeOption) - - severities, _ := query.FindSeverity(db, logger) - severityOptions := createOptionBlockObjectsForSeverity(severities) - severityText := slack.NewTextBlockObject(slack.PlainTextType, "Severity", false, false) - severityTextPlaceholder := slack.NewTextBlockObject("plain_text", "The severity for the incident to create", false, false) - severityOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, severityTextPlaceholder, "incident_severity", severityOptions...) - severityBlock := slack.NewInputBlock("incident_severity", severityText, nil, severityOption) - - pagerDutyImpactedServiceText := slack.NewTextBlockObject("plain_text", "Pagerduty", false, false) - pagerDutyImpactedServicePlaceholder := slack.NewTextBlockObject("plain_text", "Select pagerduty impacted services", false, false) - pagerDutyImpactedServiceElement := slack.NewPlainTextInputBlockElement(pagerDutyImpactedServicePlaceholder, "pagerduty_impacted") - pagerDutyImpactedService := slack.NewInputBlock("Pagerduty impacted service", pagerDutyImpactedServiceText, nil, pagerDutyImpactedServiceElement) - pagerDutyImpactedService.Optional = true - - incidentTitleText := slack.NewTextBlockObject("plain_text", "Incident Title", false, false) - incidentTitlePlaceholder := slack.NewTextBlockObject("plain_text", "Write something", false, false) - incidentTitleElement := slack.NewPlainTextInputBlockElement(incidentTitlePlaceholder, "incident_title") - incidentTitle := slack.NewInputBlock("Incident title", incidentTitleText, nil, incidentTitleElement) - - incidentDescriptionText := slack.NewTextBlockObject("plain_text", "Incident description", false, false) - incidentDescriptionPlaceholder := slack.NewTextBlockObject("plain_text", "Write something", false, false) - incidentDescriptionElement := slack.NewPlainTextInputBlockElement(incidentDescriptionPlaceholder, "incident_description") - incidentDescriptionElement.Multiline = true - incidentDescription := slack.NewInputBlock("Incident description", incidentDescriptionText, nil, incidentDescriptionElement) - incidentDescription.Optional = true - - blocks := slack.Blocks{ - BlockSet: []slack.Block{ - headerSection, - incidentTypeBlock, - severityBlock, - pagerDutyImpactedService, - incidentTitle, - incidentDescription, - }, - } - - return slack.ModalViewRequest{ - Type: slack.ViewType("modal"), - Title: titleText, - Close: closeText, - Submit: submitText, - Blocks: blocks, - PrivateMetadata: channel, - CallbackID: "start_incident_button", - } -} - -func createOptionBlockObjectsForSeverity(options []entity.SeverityEntity) []*slack.OptionBlockObject { - optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) - for _, o := range options { - optionText := slack.NewTextBlockObject(slack.PlainTextType, o.Name, false, false) - optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(strconv.Itoa(int(o.ID)), optionText, nil)) - } - return optionBlockObjects -} - -func createOptionBlockObjectsForTeams(options []entity.TeamEntity) []*slack.OptionBlockObject { - optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) - for _, o := range options { - optionText := slack.NewTextBlockObject(slack.PlainTextType, o.Name, false, false) - optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(strconv.Itoa(int(o.ID)), optionText, nil)) - } - return optionBlockObjects -} diff --git a/pkg/slack/houston/design/blazeless_show_incidents_button_section.go b/pkg/slack/houston/design/blazeless_show_incidents_button_section.go deleted file mode 100644 index 05843f0..0000000 --- a/pkg/slack/houston/design/blazeless_show_incidents_button_section.go +++ /dev/null @@ -1,29 +0,0 @@ -package houston - -import ( - "houston/entity" - - "github.com/slack-go/slack" -) - -func GenerateModalForShowIncidentsButtonSection(incident []entity.IncidentSeverityTeamJoinEntity) []slack.Block { - contextBlock := slack.NewContextBlock("", slack.NewTextBlockObject("mrkdwn", ":eye: Only visible to you", false, false)) - - sectionBlocks := []*slack.SectionBlock{} - for i := 0; i < len(incident); i++ { - fields := []*slack.TextBlockObject{ - slack.NewTextBlockObject("mrkdwn", "\n`"+incident[i].SeverityName+"` `"+incident[i].TeamsName+" Incident` \n "+incident[i].Title+"\n <#"+incident[i].SlackChannel+">", false, false), - } - sectionBlocks = append(sectionBlocks, slack.NewSectionBlock(nil, fields, nil)) - } - - blocks := []slack.Block{ - contextBlock, - } - for i := 0; i < len(sectionBlocks); i++ { - blocks = append(blocks, sectionBlocks[i]) - } - - return blocks - -} diff --git a/pkg/slack/houston/design/blazeless_summary_section.go b/pkg/slack/houston/design/blazeless_summary_section.go deleted file mode 100644 index 30739c2..0000000 --- a/pkg/slack/houston/design/blazeless_summary_section.go +++ /dev/null @@ -1,80 +0,0 @@ -package houston - -import ( - "fmt" - "houston/entity" - "houston/pkg/postgres/query" - - "github.com/slack-go/slack" - "gorm.io/gorm" -) - -func SummarySection(incidentEntity *entity.IncidentEntity) []slack.Block { - return []slack.Block{ - buildSummaryHeader(incidentEntity.Title), - buildTypeAndChannelSectionBlock(incidentEntity, "", ""), - buildSeverityAndTicketSectionBlock(incidentEntity), - buildStatusAndMeetingSectionBlock(incidentEntity), - } -} - -func IncidentSummarySection(incidentEntity *entity.IncidentEntity, db *gorm.DB) (slack.Blocks, error) { - teamEntity, err := query.FindTeamById(db, incidentEntity.TeamsId) - if err != nil { - return slack.Blocks{}, fmt.Errorf("error in searching team with id: %d, err: %v", incidentEntity.TeamsId, err) - } - - severityEntity, err := query.FindSeverityById(db, incidentEntity.SeverityId) - if err != nil { - return slack.Blocks{}, fmt.Errorf("error in searching severity with id: %d, err: %v", incidentEntity.SeverityId, err) - } - return slack.Blocks{ - BlockSet: []slack.Block{ - buildTypeAndChannelSectionBlock(incidentEntity, teamEntity.Name, severityEntity.Name)}, - }, nil -} - -func buildSummaryHeader(incidentTitle string) *slack.SectionBlock { - headerText := slack.NewTextBlockObject("plain_text", incidentTitle, true, true) - headerSection := slack.NewSectionBlock(headerText, nil, nil) - - return headerSection -} - -func buildTypeAndChannelSectionBlock(incidentEntity *entity.IncidentEntity, teamName, severityName string) *slack.SectionBlock { - fields := []*slack.TextBlockObject{ - slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*<@%s>* \n*%s* - *%s*\n", incidentEntity.CreatedBy, incidentEntity.IncidentName, incidentEntity.Title), false, false), - slack.NewTextBlockObject("mrkdwn", "\n", false, false), - slack.NewTextBlockObject("mrkdwn", incidentEntity.Description, false, false), - slack.NewTextBlockObject("mrkdwn", "\n", false, false), - slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Type*\n%s", teamName), false, false), - slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Channel*\n<#%s>", incidentEntity.SlackChannel), false, false), - slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Severity*\n%s", severityName), false, false), - slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Ticket*\n%s", "Integration Disabled"), false, false), - slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Status*\n%s", incidentEntity.Status), false, false), - slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Meeting*\n%s", "Integration Disabled"), false, false), - } - block := slack.NewSectionBlock(nil, fields, nil) - - return block -} - -func buildSeverityAndTicketSectionBlock(incidentEntity *entity.IncidentEntity) *slack.SectionBlock { - fields := []*slack.TextBlockObject{ - slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Severity*\n%d", incidentEntity.SeverityId), false, false), - slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Ticket*\n%s", "integration disabled"), false, false), - } - block := slack.NewSectionBlock(nil, fields, nil) - - return block -} - -func buildStatusAndMeetingSectionBlock(incidentEntity *entity.IncidentEntity) *slack.SectionBlock { - fields := []*slack.TextBlockObject{ - slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Status*\n%s", incidentEntity.Status), false, false), - slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Meeting*\n%s", "integration disabled"), false, false), - } - block := slack.NewSectionBlock(nil, fields, nil) - - return block -} diff --git a/pkg/slack/houston/design/incident_update_tags.go b/pkg/slack/houston/design/incident_update_tags.go deleted file mode 100644 index 3604047..0000000 --- a/pkg/slack/houston/design/incident_update_tags.go +++ /dev/null @@ -1,105 +0,0 @@ -package houston - -import ( - "fmt" - "houston/entity" - "strconv" - "strings" - - "github.com/slack-go/slack" -) - -func BuildIncidentUpdateTagModal(channel slack.Channel, tags []entity.TagsEntity, teamName string, savedTags []entity.TagsEntity, - savedContributingFactor *entity.ContributingFactorEntity, contributingFactors []entity.ContributingFactorEntity, - savedCustomerTags []entity.CustomerTagsEntity, customerTags []entity.CustomerTagsEntity, savedDataPlatformTags *entity.IncidentsTagsDataPlatformMapping) slack.ModalViewRequest { - - titleText := slack.NewTextBlockObject("plain_text", "Edit Tags", false, false) - closeText := slack.NewTextBlockObject("plain_text", "Close", false, false) - submitText := slack.NewTextBlockObject("plain_text", "Submit", false, false) - - contributingFactorBlockOption := createContributingFactorBlocks(contributingFactors) - contributingFactorText := slack.NewTextBlockObject(slack.PlainTextType, "CONTRIBUTING-FACTORS", false, false) - contributingFactorPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, fmt.Sprint("SELECT CONTRIBUTING-FACTORS tags"), false, false) - contributingFactorOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, contributingFactorPlaceholder, "incident_cf_modal_request", contributingFactorBlockOption...) - if savedContributingFactor != nil { - contributingFactorOption.InitialOption = slack.NewOptionBlockObject(strconv.FormatUint(uint64(savedContributingFactor.ID), 10), slack.NewTextBlockObject(slack.PlainTextType, savedContributingFactor.Label, false, false), nil) - } - contributingFactorHint := slack.NewTextBlockObject(slack.PlainTextType, "CONTRIBUTING-FACTORS is configured to have one single tag, please select appropriate single tag from the dropdown", false, false) - contributingFactorBlock := slack.NewInputBlock("incident_cf_modal_request_input", contributingFactorText, contributingFactorHint, contributingFactorOption) - contributingFactorBlock.Optional = true - - customerBlockOption := createCustomerBlocks(customerTags) - customerText := slack.NewTextBlockObject(slack.PlainTextType, "CUSTOMER", false, false) - customerPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, fmt.Sprint("SELECT CUSTOMER tags"), false, false) - customerOption := slack.NewOptionsMultiSelectBlockElement(slack.MultiOptTypeStatic, customerPlaceholder, "incident_customer_tags_modal_request", customerBlockOption...) - customerOption.InitialOptions = createCustomerBlocks(savedCustomerTags) - customerBlock := slack.NewInputBlock("incident_customer_tags_modal_request_input", customerText, nil, customerOption) - customerBlock.Optional = true - - incidentStatusBlockOption := createIncidentTagsBlock(tags) - incidentTagText := slack.NewTextBlockObject(slack.PlainTextType, fmt.Sprint("CUSTOMER: "+strings.ToUpper(teamName)), false, false) - incidentTagPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, fmt.Sprint("SELECT CUSTOMER: "+strings.ToUpper(teamName)+" tags"), false, false) - incidentTypeOption := slack.NewOptionsMultiSelectBlockElement(slack.MultiOptTypeStatic, incidentTagPlaceholder, "incident_tags_modal_request", incidentStatusBlockOption...) - incidentTypeOption.InitialOptions = createIncidentTagsBlock(savedTags) - incidentTypeBlock := slack.NewInputBlock("incident_tags_modal_request_input", incidentTagText, nil, incidentTypeOption) - incidentTypeBlock.Optional = true - - dataPlatformText := slack.NewTextBlockObject("plain_text", "DATA PLATFORM", false, false) - dataPlatformPlaceholder := slack.NewTextBlockObject("plain_text", "Comma seperated list of tags", false, false) - dataPlatformElement := slack.NewPlainTextInputBlockElement(dataPlatformPlaceholder, "incident_data_platform_tag_modal_request") - if savedDataPlatformTags != nil { - dataPlatformElement.InitialValue = savedDataPlatformTags.DataPlatformTag - } - dataPlatformTitle := slack.NewInputBlock("DATA_PLATFORM", dataPlatformText, nil, dataPlatformElement) - dataPlatformTitle.Optional = true - - blocks := slack.Blocks{ - BlockSet: []slack.Block{ - contributingFactorBlock, - customerBlock, - incidentTypeBlock, - dataPlatformTitle, - }, - } - - return slack.ModalViewRequest{ - Type: slack.ViewType("modal"), - Title: titleText, - Close: closeText, - Submit: submitText, - Blocks: blocks, - PrivateMetadata: channel.ID, - CallbackID: "updateTag", - } - -} - -func createIncidentTagsBlock(options []entity.TagsEntity) []*slack.OptionBlockObject { - optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) - for _, o := range options { - txt := fmt.Sprintf(o.Label) - optionText := slack.NewTextBlockObject(slack.PlainTextType, txt, false, false) - optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(strconv.FormatUint(uint64(o.ID), 10), optionText, nil)) - } - return optionBlockObjects -} - -func createContributingFactorBlocks(options []entity.ContributingFactorEntity) []*slack.OptionBlockObject { - optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) - for _, o := range options { - txt := fmt.Sprintf(o.Label) - optionText := slack.NewTextBlockObject(slack.PlainTextType, txt, false, false) - optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(strconv.FormatUint(uint64(o.ID), 10), optionText, nil)) - } - return optionBlockObjects -} - -func createCustomerBlocks(options []entity.CustomerTagsEntity) []*slack.OptionBlockObject { - optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) - for _, o := range options { - txt := fmt.Sprintf("%s", o.Label) - optionText := slack.NewTextBlockObject(slack.PlainTextType, txt, false, false) - optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(strconv.FormatUint(uint64(o.ID), 10), optionText, nil)) - } - return optionBlockObjects -} diff --git a/pkg/slack/houston/slash_command_processor.go b/pkg/slack/houston/slash_command_processor.go deleted file mode 100644 index 9e719b0..0000000 --- a/pkg/slack/houston/slash_command_processor.go +++ /dev/null @@ -1,201 +0,0 @@ -package houston - -import ( - "fmt" - "houston/entity" - "houston/pkg/postgres/query" - "houston/pkg/slack/houston/command" - houston "houston/pkg/slack/houston/design" - - "github.com/slack-go/slack" - "github.com/slack-go/slack/slackevents" - "github.com/slack-go/slack/socketmode" - "go.uber.org/zap" - "gorm.io/gorm" -) - -type HoustonCommandHandler struct { - logger *zap.Logger - socketmodeClient *socketmode.Client - db *gorm.DB -} - -func NewHoustonCommandHandler(socketmodeClient *socketmode.Client, logger *zap.Logger, db *gorm.DB) *HoustonCommandHandler { - return &HoustonCommandHandler{ - socketmodeClient: socketmodeClient, - logger: logger, - db: db, - } -} - -func (bch *HoustonCommandHandler) ProcessSlashCommand(evt socketmode.Event) { - HoustonMainCommand(bch.db, bch.socketmodeClient, bch.logger, &evt) - -} - -func (bch *HoustonCommandHandler) ProcessCallbackEvent(callback slack.InteractionCallback) { - bch.logger.Info("process callback event", zap.Any("callback", callback)) -} - -func (bch *HoustonCommandHandler) ProcessModalCallbackEvent(callback slack.InteractionCallback, request *socketmode.Request) { - var callbackId = callback.View.CallbackID - switch callbackId { - case "start_incident_button": - cip := command.NewCreateIncidentProcessor(bch.socketmodeClient, bch.logger, bch.db) - cip.CreateIncidentModalCommandProcessing(callback, request) - case "assignIncidentRole": - iap := command.NewIncidentAssignProcessor(bch.socketmodeClient, bch.db, bch.logger) - iap.IncidentAssignModalCommandProcessing(callback, request) - case "setIncidentStatus": - isp := command.NewIncidentUpdateStatusProcessor(bch.socketmodeClient, bch.db, bch.logger) - isp.IncidentUpdateStatus(callback, request, callback.Channel, callback.User) - case "setIncidentTitle": - itp := command.NewIncidentUpdateTitleProcessor(bch.socketmodeClient, bch.db, bch.logger) - itp.IncidentUpdateTitle(callback, request, callback.Channel, callback.User) - case "setIncidentDescription": - idp := command.NewIncidentUpdateDescriptionProcessor(bch.socketmodeClient, bch.db, bch.logger) - idp.IncidentUpdateDescription(callback, request, callback.Channel, callback.User) - case "setIncidentSeverity": - isp := command.NewIncidentUpdateSeverityProcessor(bch.socketmodeClient, bch.db, bch.logger) - isp.IncidentUpdateSeverity(callback, request, callback.Channel, callback.User) - case "setIncidentType": - itp := command.NewIncidentUpdateTypeProcessor(bch.socketmodeClient, bch.db, bch.logger) - itp.IncidentUpdateType(callback, request, callback.Channel, callback.User) - case "updateTag": - itp := command.NewIncidentUpdateTagsProcessor(bch.socketmodeClient, bch.db, bch.logger) - itp.IncidentUpdateTags(callback, request, callback.Channel, callback.User) - } - incidentEntity, _ := query.FindIncidentByChannelId(bch.db, callback.View.PrivateMetadata) - if incidentEntity != nil { - UpdateMessage(bch.db, incidentEntity, bch.socketmodeClient, nil) - } -} - -func UpdateMessage(db *gorm.DB, incidentEntity *entity.IncidentEntity, socketmodeClient *socketmode.Client, slackClient *slack.Client) { - messages, _ := query.FindMessageByIncidentName(db, incidentEntity.IncidentName) - blocks, _ := houston.IncidentSummarySection(incidentEntity, db) - color := query.GetColorBySeverity(incidentEntity.SeverityId) - att := slack.Attachment{Blocks: blocks, Color: color} - for _, message := range messages { - if socketmodeClient != nil { - socketmodeClient.UpdateMessage(message.SlackChannel, message.MessageTimeStamp, slack.MsgOptionAttachments(att)) - } else { - slackClient.UpdateMessage(message.SlackChannel, message.MessageTimeStamp, slack.MsgOptionAttachments(att)) - } - } -} - -func (bch *HoustonCommandHandler) ProcessButtonHandler(callback slack.InteractionCallback, request *socketmode.Request) { - actionId := callback.ActionCallback.BlockActions[0].ActionID - bch.logger.Info("process button callback event", zap.Any("action_id", actionId), - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - - switch actionId { - case "start_incident_button": - bch.logger.Info("start incident button command received", - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - sip := command.NewStartIncidentProcessor(bch.socketmodeClient, bch.db, bch.logger) - sip.ProcessStartIncidentButtonCommand(bch.socketmodeClient, request, callback.Channel.ID, callback.TriggerID) - case "show_incidents_button": - bch.logger.Info("show incidents button command received", - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - sip := command.ShowIncidentsProcessor(bch.socketmodeClient, bch.db, bch.logger) - sip.ProcessShowIncidentsButtonCommand(callback.Channel, callback.User, callback.TriggerID, request) - case "incident": - bch.processIncidentCommands(callback, request) - case "tags": - bch.processTagsCommands(callback, request) - default: - msgOption := slack.MsgOptionText(fmt.Sprintf("We are working on it"), false) - _, err := bch.socketmodeClient.PostEphemeral(callback.Channel.ID, callback.User.ID, msgOption) - if err != nil { - bch.logger.Error("houston slack PostEphemeral command failed for Working On It features", - zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } - var payload interface{} - bch.socketmodeClient.Ack(*request, payload) - } -} - -func (bch *HoustonCommandHandler) ProcessMemberJoinEvent(memberJoinedChannelEvent *slackevents.MemberJoinedChannelEvent, request *socketmode.Request) { - memberJoinProcessor := command.NewMemberJoinProcessor(bch.socketmodeClient, bch.db, bch.logger) - memberJoinProcessor.MemberJoinProcessCommand(memberJoinedChannelEvent) - var payload interface{} - bch.socketmodeClient.Ack(*request, payload) -} - -func (bch *HoustonCommandHandler) processIncidentCommands(callback slack.InteractionCallback, request *socketmode.Request) { - action := callback.ActionCallback.BlockActions[0].SelectedOption.Value - switch action { - case "assignIncidentRole": - bch.logger.Info("incident assign button command received", - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - iap := command.NewIncidentAssignProcessor(bch.socketmodeClient, bch.db, bch.logger) - iap.IncidentAssignProcess(callback, request) - case "resolveIncident": - bch.logger.Info("incident update button command received", - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - irp := command.NewIncidentResolveProcessor(bch.socketmodeClient, bch.db, bch.logger) - irp.IncidentResolveProcess(callback, request) - case "setIncidentStatus": - bch.logger.Info("incident update status command received", - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - isp := command.NewIncidentUpdateStatusProcessor(bch.socketmodeClient, bch.db, bch.logger) - isp.IncidentUpdateStatusRequestProcess(callback, request) - case "setIncidentType": - bch.logger.Info("incident update type command received", - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - itp := command.NewIncidentUpdateTypeProcessor(bch.socketmodeClient, bch.db, bch.logger) - itp.IncidentUpdateTypeRequestProcess(callback, request) - case "setIncidentSeverity": - bch.logger.Info("incident update severity command received", - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - itp := command.NewIncidentUpdateSeverityProcessor(bch.socketmodeClient, bch.db, bch.logger) - itp.IncidentUpdateSeverityRequestProcess(callback, request) - case "setIncidentTitle": - bch.logger.Info("incident update title command received", - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - itp := command.NewIncidentUpdateTitleProcessor(bch.socketmodeClient, bch.db, bch.logger) - itp.IncidentUpdateTitleRequestProcess(callback, request) - case "setIncidentDescription": - bch.logger.Info("incident update description command received", - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - idp := command.NewIncidentUpdateDescriptionProcessor(bch.socketmodeClient, bch.db, bch.logger) - idp.IncidentUpdateDescriptionRequestProcess(callback, request) - } -} - -func (bch *HoustonCommandHandler) processTagsCommands(callback slack.InteractionCallback, request *socketmode.Request) { - action := callback.ActionCallback.BlockActions[0].SelectedOption.Value - switch action { - case "addTags": - bch.logger.Info("Add Tags command received", - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - itp := command.NewIncidentUpdateTagsProcessor(bch.socketmodeClient, bch.db, bch.logger) - itp.IncidentUpdateTagsRequestProcess(callback, request) - case "showTags": - bch.logger.Info("Show Tags command received", - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - itp := command.NewIncidentShowTagsProcessor(bch.socketmodeClient, bch.db, bch.logger) - itp.IncidentShowTagsRequestProcess(callback, request) - case "removeTag": - bch.logger.Info("Remove Tags command received", - zap.String("channel", callback.Channel.Name), zap.String("user_id", callback.User.ID), - zap.String("user_name", callback.User.Name)) - itp := command.NewIncidentUpdateTagsProcessor(bch.socketmodeClient, bch.db, bch.logger) - itp.IncidentUpdateTagsRequestProcess(callback, request) - } -} diff --git a/pkg/slack/.DS_Store b/pkg/slackbot/.DS_Store similarity index 100% rename from pkg/slack/.DS_Store rename to pkg/slackbot/.DS_Store diff --git a/pkg/slackbot/channel.go b/pkg/slackbot/channel.go new file mode 100644 index 0000000..c9e41c3 --- /dev/null +++ b/pkg/slackbot/channel.go @@ -0,0 +1,48 @@ +package slackbot + +import ( + "fmt" + "github.com/slack-go/slack" + "go.uber.org/zap" +) + +func (c *Client) FindParticipants(channelId string) ([]string, error) { + request := &slack.GetUsersInConversationParameters{ + ChannelID: channelId, + Limit: 1000, + } + channelInfo, _, err := c.socketModeClient.GetUsersInConversation(request) + if err != nil { + c.logger.Error("find participants failed", zap.String("channel_id", channelId), zap.Error(err)) + return nil, fmt.Errorf("fetch channel conversationInfo failed. err: %v", err) + } + + return channelInfo, nil +} + +func (c *Client) CreateChannel(channelName string) (string, error) { + request := slack.CreateConversationParams{ + ChannelName: channelName, + IsPrivate: false, + } + + channel, err := c.socketModeClient.CreateConversation(request) + if err != nil { + c.logger.Error("create slackbot channel failed", zap.String("channel_name", channelName), zap.Error(err)) + return "", err + } + + c.logger.Info("created slackbot channel successfully", zap.String("channel_name", channelName), zap.String("channel_id", channel.ID)) + return channel.ID, nil +} + +func (c *Client) InviteUsersToConversation(channelId string, userId ...string) { + _, err := c.socketModeClient.InviteUsersToConversation(channelId, userId...) + if err != nil { + c.logger.Error("invite users to conversation failed", + zap.String("channel_id", channelId), zap.Any("user_ids", userId), zap.Error(err)) + return + } + + c.logger.Info("successfully invite users to conversation", zap.String("channel_id", channelId), zap.Any("user_ids", userId)) +} diff --git a/pkg/slackbot/config.go b/pkg/slackbot/config.go new file mode 100644 index 0000000..5b64551 --- /dev/null +++ b/pkg/slackbot/config.go @@ -0,0 +1,18 @@ +package slackbot + +import ( + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" +) + +type Client struct { + socketModeClient *socketmode.Client + logger *zap.Logger +} + +func NewSlackClient(logger *zap.Logger, socketModeClient *socketmode.Client) *Client { + return &Client{ + socketModeClient: socketModeClient, + logger: logger, + } +} diff --git a/schema.sql b/schema.sql index ecf838a..3a72e13 100644 --- a/schema.sql +++ b/schema.sql @@ -1,53 +1,53 @@ -CREATE TABLE tenant ( - id SERIAL PRIMARY KEY, - name text, - key text, - url text, - vertical text, - version bigint default 0, - created_at timestamp without time zone, - updated_at timestamp without time zone, - deleted_at timestamp without time zone -); - -CREATE TABLE grafana ( - id SERIAL PRIMARY KEY, - name text, - url text, - tenant_id bigint, - status boolean DEFAULT false, - alert_id bigint, - version bigint default 0, - created_at timestamp without time zone, - updated_at timestamp without time zone, - deleted_at timestamp without time zone -); - -CREATE TABLE alerts ( - id SERIAL PRIMARY KEY, - name text, - team_name text, - service_name text, - status boolean DEFAULT false, - version bigint default 0, - created_at timestamp without time zone, - updated_at timestamp without time zone, - deleted_at timestamp without time zone -); +-- CREATE TABLE tenant ( +-- id SERIAL PRIMARY KEY, +-- name text, +-- key text, +-- url text, +-- vertical text, +-- version bigint default 0, +-- created_at timestamp without time zone, +-- updated_at timestamp without time zone, +-- deleted_at timestamp without time zone +-- ); +-- +-- CREATE TABLE grafana ( +-- id SERIAL PRIMARY KEY, +-- name text, +-- url text, +-- tenant_id bigint, +-- status boolean DEFAULT false, +-- alert_id bigint, +-- version bigint default 0, +-- created_at timestamp without time zone, +-- updated_at timestamp without time zone, +-- deleted_at timestamp without time zone +-- ); +-- +-- CREATE TABLE alerts ( +-- id SERIAL PRIMARY KEY, +-- name text, +-- team_name text, +-- service_name text, +-- status boolean DEFAULT false, +-- version bigint default 0, +-- created_at timestamp without time zone, +-- updated_at timestamp without time zone, +-- deleted_at timestamp without time zone +-- ); -CREATE TABLE incidents ( +CREATE TABLE incident ( id SERIAL PRIMARY KEY, title text, description text, - status varchar(50), - severity_id bigint, + status integer not null, + severity_id integer not null, incident_name text, slack_channel varchar(100), detection_time timestamp without time zone, - customer_impact_start_time timestamp without time zone, - customer_impact_end_time timestamp without time zone, - teams_id bigint, + start_time timestamp without time zone, + end_time timestamp without time zone, + team_id int not null, jira_id varchar(100), confluence_id varchar(100), created_by varchar(100), @@ -57,17 +57,13 @@ CREATE TABLE incidents ( enable_reminder boolean DEFAULT false, created_at timestamp without time zone, updated_at timestamp without time zone, - deleted_at timestamp without time zone, - version bigint default 0 + deleted_at timestamp without time zone ); -CREATE TABLE teams ( +CREATE TABLE team ( id SERIAL PRIMARY KEY, - name varchar(50), - oncall_handle varchar(100), - secondary_oncall_handle varchar(100), - manager_handle varchar(100), - secondary_manager_handle varchar(100), + name varchar(50) unique not null, + slack_user_ids varchar[] default '{}', active boolean DEFAULT false, version bigint default 0, created_at timestamp without time zone, @@ -75,112 +71,52 @@ CREATE TABLE teams ( deleted_at timestamp without time zone ); -CREATE TABLE teams_severity_user_mapping ( - id SERIAL PRIMARY KEY, - entity_type varchar(100), - entity_id bigint, - users_id bigint, - version bigint default 0, - default_add_in_incidents boolean DEFAULT false, - team_role varchar(100), +create table team_tag ( + id serial primary key, + team_id int not null, + tag_id int not null, + optional boolean default false, created_at timestamp without time zone, updated_at timestamp without time zone, deleted_at timestamp without time zone ); - -CREATE TABLE users ( +CREATE TABLE houston_user ( id SERIAL PRIMARY KEY, name varchar(50), slack_user_id varchar(100), active boolean DEFAULT true ); - CREATE TABLE severity ( id SERIAL PRIMARY KEY, name varchar(50), description text, version bigint default 0, sla int, + slack_user_ids varchar[] default '{}', created_at timestamp without time zone, updated_at timestamp without time zone, deleted_at timestamp without time zone ); -CREATE TABLE teams_tags_mapping ( +CREATE TABLE tag ( id SERIAL PRIMARY KEY, - teams_id bigint, - tag_id bigint, - version bigint default 0, + name varchar not null, + label text not null, + place_holder text, + action_id varchar(100) not null, + type varchar(100) not null, created_at timestamp without time zone, updated_at timestamp without time zone, deleted_at timestamp without time zone ); -CREATE TABLE incidents_tags_mapping ( - id SERIAL PRIMARY KEY, - incident_id bigint, - tag_id bigint, - version bigint default 0, - created_at timestamp without time zone, - updated_at timestamp without time zone, - deleted_at timestamp without time zone -); - -CREATE TABLE incidents_tags_contributing_factor_mapping ( - id SERIAL PRIMARY KEY, - incident_id bigint, - contributing_factor_id bigint, - version bigint default 0, - created_at timestamp without time zone, - updated_at timestamp without time zone, - deleted_at timestamp without time zone -); - -CREATE TABLE contributing_factor ( - id SERIAL PRIMARY KEY, - label varchar(100), - version bigint default 0, - created_at timestamp without time zone, - updated_at timestamp without time zone, - deleted_at timestamp without time zone -); - -CREATE TABLE incidents_tags_customer_mapping ( - id SERIAL PRIMARY KEY, - incident_id bigint, - customer_tags_id bigint, - version bigint default 0, - created_at timestamp without time zone, - updated_at timestamp without time zone, - deleted_at timestamp without time zone -); - -CREATE TABLE customer_tags ( - id SERIAL PRIMARY KEY, - label varchar(100), - version bigint default 0, - created_at timestamp without time zone, - updated_at timestamp without time zone, - deleted_at timestamp without time zone -); - -CREATE TABLE incidents_tags_data_platform_mapping ( - id SERIAL PRIMARY KEY, - incident_id bigint, - data_platform_tag text, - version bigint default 0, - created_at timestamp without time zone, - updated_at timestamp without time zone, - deleted_at timestamp without time zone -); - -CREATE TABLE tags ( - id SERIAL PRIMARY KEY, - label text, - version bigint default 0, - created_at timestamp without time zone, +create table tag_value ( + id serial primary key, + tag_id int not null, + value varchar not null, + create_at timestamp without time zone, updated_at timestamp without time zone, deleted_at timestamp without time zone ); @@ -189,28 +125,36 @@ CREATE TABLE incident_status ( id SERIAL PRIMARY KEY, name varchar(50), description text, + is_terminal_status boolean default false, version bigint default 0, created_at timestamp without time zone, updated_at timestamp without time zone, deleted_at timestamp without time zone ); -CREATE TABLE incident_roles ( +create table role ( + id serial primary key, + name varchar(100) not null, + created_at timestamp without time zone, + updated_at timestamp without time zone, + deleted_at timestamp without time zone +); + +CREATE TABLE incident_role ( id SERIAL PRIMARY KEY, - incident_id bigint, + incident_id integer not null, role varchar(100), - assigned_to_user_slack_id varchar(100), - assigned_by_user_slack_id varchar(100), - version bigint default 0, + assigned_to varchar(100), + assigned_by varchar(100), created_at timestamp without time zone, updated_at timestamp without time zone, deleted_at timestamp without time zone ); -CREATE TABLE messages ( +CREATE TABLE incident_channel ( id SERIAL PRIMARY KEY, slack_channel varchar(100), - incident_name text, + incident_id int not null, message_timestamp varchar(100), version bigint default 0, created_at timestamp without time zone, @@ -218,14 +162,25 @@ CREATE TABLE messages ( deleted_at timestamp without time zone ); -CREATE TABLE audit ( - id SERIAL PRIMARY KEY, - incident_id bigint, - event text, - user_name varchar(100), - user_id varchar(100), - version bigint default 0, +create table incident_tag ( + id serial primary key, + incident_id int not null, + tag_id int not null, + tag_value_ids int[] default '{}', + free_text_value text, created_at timestamp without time zone, updated_at timestamp without time zone, deleted_at timestamp without time zone -); \ No newline at end of file +); + +-- CREATE TABLE audit ( +-- id SERIAL PRIMARY KEY, +-- incident_id bigint, +-- event text, +-- user_name varchar(100), +-- user_id varchar(100), +-- version bigint default 0, +-- created_at timestamp without time zone, +-- updated_at timestamp without time zone, +-- deleted_at timestamp without time zone +-- ); \ No newline at end of file