diff --git a/cmd/app/handler/slack_handler.go b/cmd/app/handler/slack_handler.go index cb1de9d..577a10b 100644 --- a/cmd/app/handler/slack_handler.go +++ b/cmd/app/handler/slack_handler.go @@ -53,7 +53,17 @@ func NewSlackHandler(gormClient *gorm.DB, socketModeClient *socketmode.Client) * incidentServiceV2 := incidentServiceV2.NewIncidentServiceV2(gormClient) slackService := slack2.NewSlackService() - cron.RunJob(socketModeClient, gormClient, incidentService, severityService, teamService, shedlockService, userService) + cron.RunJob( + socketModeClient, + gormClient, + incidentService, + severityService, + teamService, + shedlockService, + userService, + incidentServiceV2, + slackService, + ) return &slackHandler{ socketModeClient: socketModeClient, diff --git a/common/util/common_util.go b/common/util/common_util.go index ca662a1..be2147b 100644 --- a/common/util/common_util.go +++ b/common/util/common_util.go @@ -28,6 +28,25 @@ func RemoveDuplicate[T string | int](sliceList []T) []T { return list } +// GetTimeElapsedInDaysAndHours - returns number of days and hours elapsed since the timestamp passed in argument +func GetTimeElapsedInDaysAndHours(timestamp time.Time) (int, int) { + // convert to IST time + locationName := "Asia/Kolkata" + location, err := time.LoadLocation(locationName) + if err != nil { + logger.Error(fmt.Sprintf("failed to load location for locationName %s", locationName)) + } + timestampInIST := timestamp.In(location) + currentTimeInIST := time.Now().In(location) + // Calculate the time difference + timeDiff := currentTimeInIST.Sub(timestampInIST) + + // Convert the duration into days and hours + days := int(timeDiff.Hours() / 24) + hours := int(timeDiff.Hours()) % 24 + return days, hours +} + // Difference : finds difference of two slices and returns a new slice func Difference(s1, s2 []string) []string { combinedSlice := append(s1, s2...) diff --git a/config/application.properties b/config/application.properties index 7754cd4..9d66819 100644 --- a/config/application.properties +++ b/config/application.properties @@ -28,6 +28,9 @@ cron.job.sla_breach_interval=CRON_JOB_SLA_BREACH_INTERVAL cron.job.archive_incident_channels=CRON_JOB_ARCHIVE_INCIDENT_CHANNELS cron.job.archive_incident_channels_interval=CRON_JOB_ARCHIVE_INCIDENT_CHANNELS_INTERVAL cron.job.archival_time_in_hour=CRON_JOB_ARCHIVAL_TIME_IN_HOUR +# incident reminder Slack DMs cron +cron.job.incident_reminder=CRON_JOB_INCIDENT_REMINDER +cron.job.incident_reminder_interval=CRON_JOB_INCIDENT_REMINDER_INTERVAL #incidents incidents.show.limit=INCIDENTS_SHOW_LIMIT diff --git a/go.mod b/go.mod index f449923..758a45e 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module houston go 1.19 require ( + github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/aws/aws-sdk-go v1.44.262 github.com/aws/aws-sdk-go-v2/config v1.18.25 github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 diff --git a/internal/cron/cron.go b/internal/cron/cron.go index c8ae5a3..ba7d3f4 100644 --- a/internal/cron/cron.go +++ b/internal/cron/cron.go @@ -3,7 +3,12 @@ package cron import ( "fmt" "houston/logger" + incidentService "houston/service/incident" + slackService "houston/service/slack" + "sort" "strconv" + "strings" + "sync" "time" "houston/common/util" @@ -22,49 +27,67 @@ import ( "gorm.io/gorm" ) -func RunJob(socketModeClient *socketmode.Client, db *gorm.DB, incidentService *incident.Repository, severityService *severity.Repository, teamService *team.Repository, shedlockService *shedlock.Repository, userService *user.Repository) { +func RunJob( + socketModeClient *socketmode.Client, + db *gorm.DB, + incidentRepository *incident.Repository, + severityRepository *severity.Repository, + teamRepository *team.Repository, + shedlockRepository *shedlock.Repository, + userRepository *user.Repository, + incidentService *incidentService.IncidentServiceV2, + slackService *slackService.SlackService, +) { //HOUSTON ESCALATE shedlockConfig := NewLockerDbWithLockTime(viper.GetInt("cron.job.lock.default.time.in.sec")) - err := shedlockConfig.AddFun(viper.GetString("cron.job.name"), viper.GetString("cron.job.update.incident.interval"), shedlockService, func() { - UpdateIncidentByCronJob(socketModeClient, db, incidentService, teamService, severityService, viper.GetString("cron.job.name")) + err := shedlockConfig.AddFun(viper.GetString("cron.job.name"), viper.GetString("cron.job.update.incident.interval"), shedlockRepository, func() { + UpdateIncidentByCronJob(socketModeClient, db, incidentRepository, teamRepository, severityRepository, viper.GetString("cron.job.name")) }) if err != nil { logger.Error("HOUSTON_ESCALATE error: " + err.Error()) } //HOUSTON DAILY TEAM UPDATE - err = shedlockConfig.AddFun(viper.GetString("cron.job.team_metric"), viper.GetString("cron.job.team_metric_interval"), shedlockService, func() { - postTeamMetrics(socketModeClient, incidentService, teamService, severityService, viper.GetString("cron.job.team_metric")) + err = shedlockConfig.AddFun(viper.GetString("cron.job.team_metric"), viper.GetString("cron.job.team_metric_interval"), shedlockRepository, func() { + postTeamMetrics(socketModeClient, incidentRepository, teamRepository, severityRepository, viper.GetString("cron.job.team_metric")) }) if err != nil { logger.Error("HOUSTON_TEAM_METRICS error :" + err.Error()) } //HOUSTON ADDING USER - err = shedlockConfig.AddFun(viper.GetString("cron.job.upsert_user"), viper.GetString("cron.job.upsert_user_interval"), shedlockService, func() { - UpsertUsers(socketModeClient, userService) + err = shedlockConfig.AddFun(viper.GetString("cron.job.upsert_user"), viper.GetString("cron.job.upsert_user_interval"), shedlockRepository, func() { + UpsertUsers(socketModeClient, userRepository) }) if err != nil { logger.Error("HOUSTON_ADDING_USER error :" + err.Error()) } //Post SLA Breach Message to Incident Channels - err = shedlockConfig.AddFun(viper.GetString("cron.job.sla_breach"), viper.GetString("cron.job.sla_breach_interval"), shedlockService, func() { - PostSLABreachMessageToIncidentChannels(socketModeClient, teamService, incidentService, severityService) + err = shedlockConfig.AddFun(viper.GetString("cron.job.sla_breach"), viper.GetString("cron.job.sla_breach_interval"), shedlockRepository, func() { + PostSLABreachMessageToIncidentChannels(socketModeClient, teamRepository, incidentRepository, severityRepository) }) if err != nil { logger.Error("HOUSTON_SLA_BREACH error :" + err.Error()) } //Archive Incident Channels - err = shedlockConfig.AddFun(viper.GetString("cron.job.archive_incident_channels"), viper.GetString("cron.job.archive_incident_channels_interval"), shedlockService, func() { - ArchiveIncidentChannels(socketModeClient, incidentService) + err = shedlockConfig.AddFun(viper.GetString("cron.job.archive_incident_channels"), viper.GetString("cron.job.archive_incident_channels_interval"), shedlockRepository, func() { + ArchiveIncidentChannels(socketModeClient, incidentRepository) }) if err != nil { logger.Error("HOUSTON_ARCHIVE_INCIDENT_CHANNELS error :" + err.Error()) } + + //HOUSTON DAILY INCIDENT REMINDER DMs TO USERS + err = shedlockConfig.AddFun(viper.GetString("cron.job.incident_reminder"), viper.GetString("cron.job.incident_reminder_interval"), shedlockRepository, func() { + HoustonIncidentReminderToUsers(incidentService, slackService, teamRepository) + }) + if err != nil { + logger.Error("HOUSTON_INCIDENT_REMINDER_DM error :" + err.Error()) + } shedlockConfig.Start() } @@ -167,6 +190,7 @@ func updatingSevForEachInc(incidents []incident.IncidentEntity, i int, incidentS func postTeamMetrics(socketModeClient *socketmode.Client, incidentService *incident.Repository, teamService *team.Repository, severityService *severity.Repository, name string) { fmt.Println("Running Team Metrics cron at", time.Now().Format(time.RFC3339)) + channelID := viper.GetString("cron.job.team_metric_update_channel") defer func() { if r := recover(); r != nil { logger.Error(fmt.Sprintf("Exception occurred in cron: %v", r.(error))) @@ -234,7 +258,7 @@ func postTeamMetrics(socketModeClient *socketmode.Client, incidentService *incid managerHandle := teamsList[i].ManagerHandle onCallHandle := teamsList[i].OncallHandle - incidentListForTeam += fmt.Sprintf("%-32s", "`TEAM`") + fmt.Sprintf("*: %s*", teamsList[i].Name) + "\n" + incidentListForTeam += fmt.Sprintf("%-32s", "`Team`") + fmt.Sprintf("*: %s*", teamsList[i].Name) + "\n" incidentListForTeam += fmt.Sprintf("%-23s", "`Open incidents`") + fmt.Sprintf("*: %d*", incidentsOpenCount) + "\n" if onCallHandle == "" { incidentListForTeam += fmt.Sprintf("%-29s", "`On-call`") + "*: None*" + "\n" @@ -250,45 +274,37 @@ func postTeamMetrics(socketModeClient *socketmode.Client, incidentService *incid incidentListForTeam += "*List of open incidents:*\n\n" for index := 0; index < len(list); index++ { - // Calculate the time difference - currentTime := time.Now() - duration := 5*time.Hour + 30*time.Minute - newTime := currentTime.Add(duration) - timeDiff := newTime.Sub(list[index].CreatedAt) - - // Convert the duration into days and hours - days := int(timeDiff.Hours() / 24) - hours := int(timeDiff.Hours()) % 24 + days, hours := util.GetTimeElapsedInDaysAndHours(list[index].CreatedAt) if list[index].ResponderId != "" { - incidentListForTeam += fmt.Sprintf("<#%s> *Severity* `%s` Assigned to *Responder* <@%s>. *Open Since*- `%d day(s) %d hour(s)`\n", list[index].SlackChannel, list[index].Severity, list[index].ResponderId, days, hours) + incidentListForTeam += fmt.Sprintf("<#%s> *Severity* `%s` Assigned to *responder* <@%s>. *Open since*- `%d day(s) %d hour(s)`\n", list[index].SlackChannel, list[index].Severity, list[index].ResponderId, days, hours) } else if list[index].ManagerId != "" { - incidentListForTeam += fmt.Sprintf("<#%s> *Severity* `%s` Assigned to *Manager* <@%s>. *Open Since*- `%d day(s) %d hour(s)`\n", list[index].SlackChannel, list[index].Severity, list[index].ManagerId, days, hours) + incidentListForTeam += fmt.Sprintf("<#%s> *Severity* `%s` Assigned to *manager* <@%s>. *Open since*- `%d day(s) %d hour(s)`\n", list[index].SlackChannel, list[index].Severity, list[index].ManagerId, days, hours) } else { - incidentListForTeam += fmt.Sprintf("<#%s> *Severity* `%s` Assigned to *No-One*. *Open Since*- `%d day(s) %d hour(s)`\n", list[index].SlackChannel, list[index].Severity, days, hours) + incidentListForTeam += fmt.Sprintf("<#%s> *Severity* `%s` Assigned to *no-One*. *Open since*- `%d day(s) %d hour(s)`\n", list[index].SlackChannel, list[index].Severity, days, hours) } } incidentListForTeam += "\n" if incidentListForTeam != "" { msgOption := slack.MsgOptionText(incidentListForTeam, false) - _, _, errMessage := socketModeClient.PostMessage(viper.GetString("cron.job.team_metric_update_channel"), msgOption) + _, _, errMessage := socketModeClient.PostMessage(channelID, msgOption) if errMessage != nil { - logger.Error("ChannelName: ", zap.String("channel_name", viper.GetString("cron.job.team_metric_update_channel"))) + logger.Error("ChannelName: ", zap.String("channel_name", channelID)) logger.Error("PostMessage failed for cronJob ", zap.Error(errMessage)) } } - postDividerToSlack(socketModeClient) + postDividerToSlack(socketModeClient, channelID) } } -func postDividerToSlack(socketModeClient *socketmode.Client) { +func postDividerToSlack(socketModeClient *socketmode.Client, channelID string) { blocks := slack.Blocks{ BlockSet: []slack.Block{ slack.NewDividerBlock(), }, } attachment := slack.Attachment{Blocks: blocks} - _, _, err := socketModeClient.PostMessage(viper.GetString("cron.job.team_metric_update_channel"), slack.MsgOptionAttachments(attachment)) + _, _, err := socketModeClient.PostMessage(channelID, slack.MsgOptionAttachments(attachment)) if err != nil { logger.Error("failed to post separator line to slack", zap.Error(err)) } @@ -482,3 +498,233 @@ func ArchiveIncidentChannels(socketModeClient *socketmode.Client, incidentReposi } logger.Info("Finishing archiving incident channels job at", zap.String("time", time.Now().Format(time.RFC822))) } + +// HoustonIncidentReminderToUsers - sends slack DMs to users with list of Houston incidents they are part of +func HoustonIncidentReminderToUsers( + incidentService *incidentService.IncidentServiceV2, + slackService *slackService.SlackService, + teamRepo *team.Repository, +) { + logTag := "[HOUSTON_INCIDENT_REMINDER_DM]" + incidentEntities, _, err := incidentService.GetAllOpenIncidents() + if err != nil { + logger.Error(fmt.Sprintf("%s failed to get open incidents. Error: %+v", logTag, err)) + return + } + + var waitForUsersToIncidentsMapToBeUpdated sync.WaitGroup + var usersToIncidentsMap = make(map[string]incidentList) + var mutex = &sync.Mutex{} + + for _, incidentEntity := range incidentEntities { + incidentEntity := incidentEntity + waitForUsersToIncidentsMapToBeUpdated.Add(1) + go func() { + defer waitForUsersToIncidentsMapToBeUpdated.Done() + users, err := slackService.GetNonBotUsersInConversation(incidentEntity.SlackChannel) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch users for incident: %s. Error: %+v", logTag, incidentEntity.IncidentName, err)) + } else { + updateUsersToIncidentsMap(&usersToIncidentsMap, &users, &incidentEntity, mutex) + } + }() + } + + teamIDToTeamEntityMap, err := getTeamIDToTeamEntityMap(teamRepo, logTag) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to getTeamIdToTeamNameMap. Error: %+v", logTag, err)) + return + } + + waitForUsersToIncidentsMapToBeUpdated.Wait() + + logger.Info(fmt.Sprintf("%s total DMs to be sent are %d", logTag, len(usersToIncidentsMap))) + + var waitToSendAllDMs sync.WaitGroup + + for u, incidents := range usersToIncidentsMap { + waitToSendAllDMs.Add(1) + u := u + incidents := incidents + go func() { + defer waitToSendAllDMs.Done() + logger.Info(fmt.Sprintf("%s userID: %s will get list of %d incidents in DM", logTag, u, len(incidents))) + reminderDMText := getIncidentReminderMessageForUser(u, incidentService, incidents, &teamIDToTeamEntityMap, logTag) + if reminderDMText != "" { + err := slackService.SendDM(u, reminderDMText) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to send reminder DM to userID %s", logTag, u)) + } + } else { + logger.Info(fmt.Sprintf("%s no incident reminder DM to be sent", logTag)) + } + }() + } + + waitToSendAllDMs.Wait() + logger.Info(fmt.Sprintf("%s all incident reminder DMs are sent", logTag)) +} + +// getTeamIDToTeamEntityMap - returns a map of team ID to team name +func getTeamIDToTeamEntityMap(teamRepo *team.Repository, logTag string) (map[uint]team.TeamEntity, error) { + m := make(map[uint]team.TeamEntity) + teams, err := teamRepo.GetAllActiveTeams() + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch all active teams: Error %+v", logTag, err)) + return nil, fmt.Errorf("failed to fetch all active teams") + } + for _, teamEntity := range *teams { + m[teamEntity.ID] = teamEntity + } + return m, nil +} + +// updateUsersToIncidentsMap - updates the map of slack user ID to list of incidents +func updateUsersToIncidentsMap( + userToIncidentsMap *map[string]incidentList, + users *[]string, + entity *incident.IncidentEntity, + mutex *sync.Mutex, +) { + for _, u := range *users { + mutex.Lock() + if existingEntities, ok := (*userToIncidentsMap)[u]; ok { + // User exists in the map, update the slice + (*userToIncidentsMap)[u] = append(existingEntities, *entity) + } else { + // User does not exist, create a new entry in the map + (*userToIncidentsMap)[u] = incidentList{*entity} + } + mutex.Unlock() + } +} + +// getIncidentReminderMessageForUser - build the formatted reminder text message listed by severity to be sent to a user +func getIncidentReminderMessageForUser( + user string, + incidentService *incidentService.IncidentServiceV2, + incidents incidentList, + teamIDToTeamEntityMap *map[uint]team.TeamEntity, + logTag string, +) string { + if len(incidents) > 0 { + sev0List, sev1List, sev2List, sev3List := splitIncidentListBySeverity(incidents) + var messages []string + messages = append(messages, fmt.Sprintf("Hi <@%s>, you are part of *%d* open incidents\n", user, len(incidents))) + if len(sev0List) > 0 { + messages = append(messages, "*Severity : Sev-0*") + appendIncidentsToTheReminderText(user, incidentService, sev0List, &messages, teamIDToTeamEntityMap, logTag) + } + if len(sev1List) > 0 { + messages = append(messages, "*Severity : Sev-1*") + appendIncidentsToTheReminderText(user, incidentService, sev1List, &messages, teamIDToTeamEntityMap, logTag) + } + if len(sev2List) > 0 { + messages = append(messages, "*Severity : Sev-2*") + appendIncidentsToTheReminderText(user, incidentService, sev2List, &messages, teamIDToTeamEntityMap, logTag) + } + if len(sev3List) > 0 { + messages = append(messages, "*Severity : Sev-3*") + appendIncidentsToTheReminderText(user, incidentService, sev3List, &messages, teamIDToTeamEntityMap, logTag) + } + return strings.Join(messages, "\n") + } else { + return "" + } +} + +func getParticipantRole( + user string, + incidentEntity *incident.IncidentEntity, + teamEntity team.TeamEntity, + incidentRoleEntity *incident.IncidentRoleEntity, +) IncidentParticipantRole { + var role IncidentParticipantRole + if incidentEntity.CreatedBy == user { + role = Reporter + } else if incidentRoleEntity != nil && incidentRoleEntity.AssignedTo == user { + role = Responder + } else if teamEntity.ManagerHandle == user { + role = TeamManager + } else if util.Contains(teamEntity.SlackUserIds, user) { + role = TeamMember + } else { + role = Participant + } + return role +} + +// appendIncidentsToTheReminderText -appends the list of formatted incidents details to the Slack message text +func appendIncidentsToTheReminderText( + user string, + incidentService *incidentService.IncidentServiceV2, + incidents incidentList, + messages *[]string, + teamIDToTeamEntityMap *map[uint]team.TeamEntity, + logTag string, +) { + for _, i := range incidents { + days, hours := util.GetTimeElapsedInDaysAndHours(i.CreatedAt) + responder, err := incidentService.GetIncidentRoleByIncidentIdAndRole(i.ID, incident.Responder) + if err != nil { + logger.Error(fmt.Sprintf("%s eror in fetching responder for incident %s", logTag, i.IncidentName)) + } + role := getParticipantRole(user, &i, (*teamIDToTeamEntityMap)[i.TeamId], responder) + message := fmt.Sprintf( + "<#%s> * | Open since:* %-20s. *Role:* %-11s. *Team:* `%s`", + i.SlackChannel, fmt.Sprintf("`%d day(s) %d hour(s)`", days, hours), fmt.Sprintf("`%s`", role), (*teamIDToTeamEntityMap)[i.TeamId].Name, + ) + *messages = append(*messages, message) + } + *messages = append(*messages, "\n") +} + +// splitIncidentListBySeverity - splits list of incident entity into 3 lists for entities with severity sev-0, sev-1, sev-2 and sev-3 +func splitIncidentListBySeverity(list incidentList) (incidentList, incidentList, incidentList, incidentList) { + var sev0List, sev1List, sev2List, sev3List incidentList + for _, incidentEntity := range list { + if incidentEntity.SeverityId == incident.Sev0Id { + sev0List = append(sev0List, incidentEntity) + } + if incidentEntity.SeverityId == incident.Sev1Id { + sev1List = append(sev1List, incidentEntity) + } + if incidentEntity.SeverityId == incident.Sev2Id { + sev2List = append(sev2List, incidentEntity) + } + if incidentEntity.SeverityId == incident.Sev3Id { + sev3List = append(sev3List, incidentEntity) + } + } + sort.Sort(sev0List) + sort.Sort(sev1List) + sort.Sort(sev2List) + sort.Sort(sev3List) + return sev0List, sev1List, sev2List, sev3List +} + +// custom implementation of the Interface `interface` below +type incidentList []incident.IncidentEntity + +func (e incidentList) Len() int { + return len(e) +} + +// Less - sorts list of incidents based on creation time +func (e incidentList) Less(i, j int) bool { + return e[i].CreatedAt.Before(e[j].CreatedAt) +} + +func (e incidentList) Swap(i, j int) { + e[i], e[j] = e[j], e[i] +} + +type IncidentParticipantRole string + +const ( + Reporter IncidentParticipantRole = "Reporter" + Responder = "Responder" + TeamManager = "Manager" + TeamMember = "Member" + Participant = "Participant" +) diff --git a/internal/cron/cron_test.go b/internal/cron/cron_test.go new file mode 100644 index 0000000..7cc889b --- /dev/null +++ b/internal/cron/cron_test.go @@ -0,0 +1,55 @@ +package cron + +import ( + "github.com/DATA-DOG/go-sqlmock" + "github.com/joho/godotenv" + pg "gorm.io/driver/postgres" + "gorm.io/gorm" + "houston/config" + "houston/logger" + "houston/model/log" + "houston/model/team" + "houston/service/incident" + "houston/service/slack" + "testing" +) + +func testHoustonIncidentReminderToUsers(t *testing.T) { + logger.InitLogger() + config.LoadHoustonConfig() + godotenv.Load() + + mockDb, dbMock, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + dialector := pg.New(pg.Config{ + Conn: mockDb, + DriverName: "postgres", + }) + db, _ := gorm.Open(dialector, &gorm.Config{}) + + statusRow := + dbMock.NewRows([]string{ + "id", "name", "description", "is_terminal_status", "version", "created_at", "updated_at", "deleted_at", + }).AddRow( + "1", "Investigating", "test description", "FALSE", "0", nil, nil, nil, + ).AddRow( + "2", "Identified", "test description", "FALSE", "0", nil, nil, nil, + ).AddRow( + "3", "Monitoring", "test description", "FALSE", "0", nil, nil, nil, + ).AddRow( + "4", "Resolved", "test description", "TRUE", "0", nil, nil, nil, + ).AddRow( + "5", "Duplicated", "test description", "TRUE", "0", nil, nil, nil, + ) + + getNonTerminalStatusesQuery := `^SELECT (.+) FROM "incident_status" WHERE "is_terminal_status" = FALSE AND "deleted_at" IS NULL$` + + dbMock.ExpectQuery(getNonTerminalStatusesQuery).WillReturnRows(statusRow) + + incidentService := incident.NewIncidentServiceV2(db) + slackService := slack.NewSlackService() + logRepository := log.NewLogRepository(db) + teamRepository := team.NewTeamRepository(db, logRepository) + logger.Info("started") + HoustonIncidentReminderToUsers(incidentService, slackService, teamRepository) + logger.Info("finished") +} diff --git a/model/incident/incident.go b/model/incident/incident.go index 1842801..5ce1e56 100644 --- a/model/incident/incident.go +++ b/model/incident/incident.go @@ -303,8 +303,64 @@ func (r *Repository) FindIncidentById(Id uint) (*IncidentEntity, error) { return &incidentEntity, nil } -func (r *Repository) FetchAllIncidents( - TeamsId []uint, SeverityIds []uint, StatusIds []uint, pageNumber int64, pageSize int64, incidentName string, from string, to string) ([]IncidentEntity, int, error) { +func (r *Repository) GetAllOpenIncidents() (*[]IncidentEntity, int, error) { + statusEntities, err := r.FetchAllNonTerminalIncidentStatuses() + if err != nil { + logger.Error(fmt.Sprintf("%s failed to fetch all non terminal incident statuses. Error: %+v", "[incident_repository]", err)) + } + var nonTerminalStatuses []uint + for _, status := range *statusEntities { + nonTerminalStatuses = append(nonTerminalStatuses, status.ID) + } + + return r.GetAllIncidents(nil, nil, nonTerminalStatuses) +} + +func (r *Repository) GetAllIncidents(teamsIds, severityIds, statusIds []uint) (*[]IncidentEntity, int, error) { + var query = r.gormClient.Model([]IncidentEntity{}) + var incidentEntity []IncidentEntity + if len(teamsIds) != 0 { + query = query.Where("team_id IN ?", teamsIds) + } + if len(severityIds) != 0 { + query = query.Where("severity_id IN ?", severityIds) + } + if len(statusIds) != 0 { + query = query.Where("status IN ?", statusIds) + } + + var totalElements int64 + result := query.Count(&totalElements) + if result.Error != nil { + return nil, 0, result.Error + } + + if result.RowsAffected == 0 { + return nil, int(totalElements), nil + } + + result = query.Order("created_at desc").Find(&incidentEntity) + + if result.Error != nil { + return nil, 0, result.Error + } + if result.RowsAffected == 0 { + return nil, int(totalElements), nil + } + + return &incidentEntity, int(totalElements), nil +} + +func (r *Repository) FetchAllIncidentsPaginated( + TeamsId []uint, + SeverityIds []uint, + StatusIds []uint, + pageNumber int64, + pageSize int64, + incidentName string, + from string, + to string, +) ([]IncidentEntity, int, error) { var query = r.gormClient.Model([]IncidentEntity{}) var incidentEntity []IncidentEntity if len(TeamsId) != 0 { diff --git a/service/incident/incident_service_v2.go b/service/incident/incident_service_v2.go index 85c329f..485c112 100644 --- a/service/incident/incident_service_v2.go +++ b/service/incident/incident_service_v2.go @@ -191,6 +191,19 @@ func (i *IncidentServiceV2) UnLinkJiraFromIncident(incidentId uint, unLinkedBy, return nil } +// GetAllOpenIncidents - returns list of all the open incidents and length of the result when success otherwise error +func (i *IncidentServiceV2) GetAllOpenIncidents() ([]incident.IncidentEntity, int, error) { + incidents, resultLength, err := i.incidentRepository.GetAllOpenIncidents() + return *incidents, resultLength, err +} + +func (i *IncidentServiceV2) GetIncidentRoleByIncidentIdAndRole( + incidentId uint, + role string, +) (*incident.IncidentRoleEntity, error) { + return i.incidentRepository.GetIncidentRoleByIncidentIdAndRole(incidentId, role) +} + const ( LinkJira string = "link" UnLinkJira = "unLink" diff --git a/service/incident_service.go b/service/incident_service.go index c055c04..1bd783e 100644 --- a/service/incident_service.go +++ b/service/incident_service.go @@ -230,7 +230,7 @@ func (i *incidentService) GetAllIncidents(incidentFilters request.IncidentFilter if incidentFilters.StatusIds != "" { StatusIds = i.SplitStringAndGetUintArray(incidentFilters.StatusIds) } - return incidentRepository.FetchAllIncidents( + return incidentRepository.FetchAllIncidentsPaginated( TeamIds, SeverityIds, StatusIds, incidentFilters.PageNumber, incidentFilters.PageSize, incidentFilters.IncidentName, incidentFilters.From, incidentFilters.To) } diff --git a/service/slack/slack_service.go b/service/slack/slack_service.go index 40dfe92..3472c82 100644 --- a/service/slack/slack_service.go +++ b/service/slack/slack_service.go @@ -26,6 +26,18 @@ func NewSlackService() *SlackService { const logTag = "[slack-service]" +// SendDM - Send a direct message to the recipient +func (s *SlackService) SendDM(recipientID string, messageText string) error { + _, _, err := s.SocketModeClient.PostMessage("@"+recipientID, slack.MsgOptionText(messageText, false)) + if err != nil { + logger.Error(fmt.Sprintf("%s error in sending DM to the recipientID: %s: %v", logTag, recipientID, err)) + return err + } + + logger.Info(fmt.Sprintf("%s DM sent to %s with text: %s", logTag, recipientID, messageText)) + return nil +} + func (s *SlackService) PostMessage(message string, escape bool, channel *slack.Channel) (string, error) { msgOption := slack.MsgOptionText(message, escape) _, timeStamp, err := s.SocketModeClient.PostMessage(channel.ID, msgOption) @@ -336,9 +348,10 @@ func (s *SlackService) GetChannelConversationHistory(channelId string) ([]slack. } return messages, nil } -func (s *SlackService) GetUsersInConversation(channelId string) ([]string, error) { + +// GetAllUsersInConversation - gets list of members in a particular Slack channel +func (s *SlackService) GetAllUsersInConversation(channelId string) ([]string, error) { users, _, err := s.SocketModeClient.GetUsersInConversation(&slack.GetUsersInConversationParameters{ - Limit: 100, ChannelID: channelId, }) if err != nil { @@ -346,6 +359,26 @@ func (s *SlackService) GetUsersInConversation(channelId string) ([]string, error } return users, nil } + +// GetNonBotUsersInConversation - gets list of non bot members in a particular Slack channel +func (s *SlackService) GetNonBotUsersInConversation(channelID string) ([]string, error) { + allUsers, err := s.GetAllUsersInConversation(channelID) + if err != nil { + return nil, err + } + allUserInfo, err := s.SocketModeClient.GetUsersInfo(allUsers...) + if err != nil { + return nil, err + } + var nonBotUsers []string + for _, u := range *allUserInfo { + if !u.IsBot { + nonBotUsers = append(nonBotUsers, u.ID) + } + } + return nonBotUsers, nil +} + func (s *SlackService) GetUsersInfo(users ...string) (*[]slack.User, error) { if len(users) == 0 { return &[]slack.User{}, nil @@ -365,9 +398,10 @@ func (s *SlackService) GetUsersInfo(users ...string) (*[]slack.User, error) { return &usersInfo, nil } + func (s *SlackService) getUserNamesInChannel(channelId string) (map[string]string, error) { var userNamesMap = make(map[string]string) - userIds, err := s.GetUsersInConversation(channelId) + userIds, err := s.GetAllUsersInConversation(channelId) if err != nil { logger.Error(fmt.Sprintf("error in getting users from channel id: %v", channelId), zap.Error(err)) return nil, err @@ -381,6 +415,7 @@ func (s *SlackService) getUserNamesInChannel(channelId string) (map[string]strin } return userNamesMap, nil } + func splitUsers(users []string, chunkSize int) [][]string { var result [][]string for index := 0; index < len(users); index += chunkSize {