From 6b44ec9ec30899e13bdb56ef26df7456ddcb948b Mon Sep 17 00:00:00 2001 From: Gullipalli Chetan Kumar Date: Fri, 20 Oct 2023 13:05:40 +0530 Subject: [PATCH] TP-43103 : Synch users data from slack to database and exposed api to return a list of bot users (#228) * TP-42310| created api service to get list of slackbots (#215) * TP-42310| created api service to get list of slackbots * minor bug fix in GetAllHoustonUserBots function * Synch users to db (#213) * TP-43103| added event listeners to user detail changes and saving to db * TP-43103| refactored the upsert users scheduler * TP-43103| fixed updating users when deactivated in scheduler * TP-43103| Made the requested changes * added info loggers to user change events * made changes to process only navi workspace user changes * made addressed changes in pr and returning Real name in GetAllHoustonUserBots api * resolved merge conflicts --- cmd/app/handler/slack_handler.go | 36 +++++--- cmd/app/server.go | 2 +- common/util/constant.go | 5 ++ common/util/user_helper.go | 33 ++++++++ config/application.properties | 4 +- internal/cron/cron.go | 29 +++++-- .../action/user_change_event_action.go | 84 +++++++++++++++++++ .../processor/events_api_event_processor.go | 34 +++++++- model/user/model.go | 4 + model/user/user.go | 36 +++++++- service/request/user_change_event_request.go | 17 ++++ service/response/user_response.go | 4 +- service/users_service.go | 23 ++++- 13 files changed, 281 insertions(+), 30 deletions(-) create mode 100644 common/util/user_helper.go create mode 100644 internal/processor/action/user_change_event_action.go create mode 100644 service/request/user_change_event_request.go diff --git a/cmd/app/handler/slack_handler.go b/cmd/app/handler/slack_handler.go index b6681f1..f78d015 100644 --- a/cmd/app/handler/slack_handler.go +++ b/cmd/app/handler/slack_handler.go @@ -2,24 +2,26 @@ package handler import ( "encoding/json" - "github.com/slack-go/slack" - "github.com/slack-go/slack/slackevents" - "github.com/slack-go/slack/socketmode" - "github.com/spf13/viper" - "go.uber.org/zap" - "gorm.io/gorm" + "houston/common/util" "houston/internal/cron" "houston/internal/diagnostic" "houston/internal/processor" "houston/internal/resolver" "houston/model/incident" - "houston/model/log" "houston/model/severity" "houston/model/shedlock" "houston/model/tag" "houston/model/team" "houston/model/user" "houston/pkg/slackbot" + + "github.com/slack-go/slack" + "github.com/slack-go/slack/slackevents" + "github.com/slack-go/slack/socketmode" + "github.com/spf13/viper" + "go.uber.org/zap" + "gorm.io/gorm" + "houston/model/log" ) type slackHandler struct { @@ -31,6 +33,7 @@ type slackHandler struct { viewSubmissionProcessor *processor.ViewSubmissionProcessor slashCommandResolver *resolver.SlashCommandResolver diagnosticCommandProcessor *processor.DiagnosticCommandProcessor + userChangeEventProcessor *processor.UserChangeEventProcessor } func NewSlackHandler(logger *zap.Logger, gormClient *gorm.DB, socketModeClient *socketmode.Client) *slackHandler { @@ -65,6 +68,9 @@ func NewSlackHandler(logger *zap.Logger, gormClient *gorm.DB, socketModeClient * slashCommandResolver: resolver.NewSlashCommandResolver( logger, diagnosticCommandProcessor, slashCommandProcessor, ), + userChangeEventProcessor: processor.NewUserChangeEventProcessor( + logger, socketModeClient, userService, + ), } } @@ -94,14 +100,22 @@ func (sh *slackHandler) HoustonConnect() { if eventErr != nil { sh.logger.Error("error occurred while serializing the event object", zap.Any("error", eventErr)) } else { - sh.logger.Info("event api", zap.String("event", string(serializedEventJson))) + if ev.InnerEvent.Type != util.UserChangeEvent { + sh.logger.Info("event api", zap.String("event", string(serializedEventJson))) + } } switch ev.Type { case slackevents.CallbackEvent: - iev := ev.InnerEvent - switch ev := iev.Data.(type) { + innerEvent := ev.InnerEvent + + switch innerEvent.Type { + case util.UserChangeEvent: + sh.userChangeEventProcessor.ProcessCommand(ev, evt.Request) + } + + switch innerEventData := innerEvent.Data.(type) { case *slackevents.MemberJoinedChannelEvent: - sh.memberJoinCallbackProcessor.ProcessCommand(ev, evt.Request) + sh.memberJoinCallbackProcessor.ProcessCommand(innerEventData, evt.Request) } } } diff --git a/cmd/app/server.go b/cmd/app/server.go index 699d8e2..bff247e 100644 --- a/cmd/app/server.go +++ b/cmd/app/server.go @@ -160,8 +160,8 @@ func (s *Server) usersHandler(houstonGroup *gin.RouterGroup) { houstonGroup.GET("/users/:id", usersHandler.GetUserInfo) houstonGroup.GET("/users", usersHandler.GetUsersInConversation) houstonGroup.GET("/users/update", usersHandler.UpdateHoustonUsers) + houstonGroup.GET("/bots", usersHandler.GetAllHoustonUserBots) } - func (s *Server) filtersHandler(houstonGroup *gin.RouterGroup) { filtersHandler := service.NewFilterService(s.gin, s.logger, s.db) diff --git a/common/util/constant.go b/common/util/constant.go index 04afc7e..af12ff0 100644 --- a/common/util/constant.go +++ b/common/util/constant.go @@ -36,3 +36,8 @@ const ( ShowIncidentSubmit = "show_incident_submit" MarkIncidentDuplicateSubmit = "mark_incident_duplicate_submit" ) + +const ( + UserChangeEvent = "user_change" + DeactivatedUserName = "Deactivated User" +) diff --git a/common/util/user_helper.go b/common/util/user_helper.go new file mode 100644 index 0000000..36d60e5 --- /dev/null +++ b/common/util/user_helper.go @@ -0,0 +1,33 @@ +package util + +import "houston/model/user" + +func UpdateUserFieldsIfChanged(existingUser *user.UserEntity, newUser *user.UserEntity) (*user.UserEntity, bool) { + isChanged := false + + if existingUser.Name != newUser.Name { + existingUser.Name = newUser.Name + isChanged = true + } + if existingUser.Email != newUser.Email { + existingUser.Email = newUser.Email + isChanged = true + } + if existingUser.Image != newUser.Image { + existingUser.Image = newUser.Image + isChanged = true + } + if existingUser.RealName != newUser.RealName { + existingUser.RealName = newUser.RealName + isChanged = true + } + if existingUser.Active != newUser.Active { + existingUser.Active = newUser.Active + isChanged = true + } + if existingUser.IsBot != newUser.IsBot { + existingUser.IsBot = newUser.IsBot + isChanged = true + } + return existingUser, isChanged +} diff --git a/config/application.properties b/config/application.properties index 1979739..35005c5 100644 --- a/config/application.properties +++ b/config/application.properties @@ -73,4 +73,6 @@ config.sa.keys=CONFIG_SA_KEYS create-incident.description.max-length=500 create-incident.title.max-length=100 -create-incident-v2-enabled=CREATE_INCIDENT_V2_ENABLED \ No newline at end of file +create-incident-v2-enabled=CREATE_INCIDENT_V2_ENABLED +#slack details +slack.workspace.id=SLACK_WORKSPACE_ID \ No newline at end of file diff --git a/internal/cron/cron.go b/internal/cron/cron.go index e8449b0..cc9d695 100644 --- a/internal/cron/cron.go +++ b/internal/cron/cron.go @@ -345,8 +345,9 @@ func buildOnCallAndManagerBlock(onCallHandle string, managerHandle string) *slac func UpsertUsers(socketModeClient *socketmode.Client, logger *zap.Logger, userService *user.Repository) { fmt.Println("Running upsertUsers job at", time.Now().Format(time.RFC3339)) + userOptions := slack.GetUsersOptionLimit(600) - slackUsers, err := socketModeClient.GetUsers() + slackUsers, err := socketModeClient.GetUsers(userOptions) if err != nil { logger.Error("socketMode client GetUsers error", zap.Error(err)) return @@ -360,6 +361,10 @@ func UpsertUsers(socketModeClient *socketmode.Client, logger *zap.Logger, userSe Name: source.Name, SlackUserId: source.ID, Active: !source.Deleted, + IsBot: source.IsBot, + Email: source.Profile.Email, + Image: source.Profile.Image32, + RealName: source.Profile.RealName, } houstonSlackUserMap[source.ID] = target } @@ -382,14 +387,22 @@ func UpsertUsers(socketModeClient *socketmode.Client, logger *zap.Logger, userSe } else { // Entry is already present in houstonSlackUserMap var dbUser = houstonDbUserMap[id] - if slackUser.Active != dbUser.Active || slackUser.Name != dbUser.Name { - dbUser.Active = slackUser.Active - dbUser.Name = slackUser.Name - logger.Info("updating user ", zap.String("id", dbUser.SlackUserId), zap.String("name", dbUser.Name), zap.Bool("active", dbUser.Active), zap.Bool("active", dbUser.Active)) - err = userService.UpdateHoustonUsers(dbUser) + // If User is Totally Deactivated, then only we need to update the Active Field + if slackUser.RealName == util.DeactivatedUserName { + dbUser.Active = false + err = userService.UpdateHoustonUser(dbUser) if err != nil { - logger.Error("UpdateHoustonUsers error", zap.Error(err)) - return + logger.Error("Error while updating Houston user", zap.String("slackId", dbUser.SlackUserId), zap.Error(err)) + } + } else { + updateDbUser, isChanged := util.UpdateUserFieldsIfChanged(&dbUser, &slackUser) + + if isChanged { + logger.Info("updating user ", zap.String("slack id", updateDbUser.SlackUserId), zap.String("name", updateDbUser.RealName)) + err = userService.UpdateHoustonUser(*updateDbUser) + if err != nil { + logger.Error("Error while updating Houston user", zap.String("slackId", dbUser.SlackUserId), zap.Error(err)) + } } } diff --git a/internal/processor/action/user_change_event_action.go b/internal/processor/action/user_change_event_action.go new file mode 100644 index 0000000..4db778c --- /dev/null +++ b/internal/processor/action/user_change_event_action.go @@ -0,0 +1,84 @@ +package action + +import ( + "encoding/json" + "fmt" + "github.com/slack-go/slack/slackevents" + "github.com/spf13/viper" + "go.uber.org/zap" + "houston/common/util" + "houston/model/user" + service "houston/service/request" +) + +type UserChangeEventAction struct { + logger *zap.Logger + userRepository *user.Repository +} + +func NewUserChangeEventAction(logger *zap.Logger, userRepository *user.Repository) *UserChangeEventAction { + return &UserChangeEventAction{ + logger: logger, + userRepository: userRepository, + } +} + +func (userChangeEventAction *UserChangeEventAction) PerformAction(event slackevents.EventsAPIEvent) { + var userChangeEventRequest service.UserChangeEventRequest + serializedEventJson, marshalError := json.Marshal(&event.InnerEvent.Data) + if marshalError != nil { + userChangeEventAction.logger.Error("error occurred while serializing the event object", zap.Any("error", marshalError)) + } + unmarshalError := json.Unmarshal(serializedEventJson, &userChangeEventRequest) + if unmarshalError != nil { + userChangeEventAction.logger.Error("error occurred while deserializing the event object", zap.Any("error", unmarshalError), zap.String("event", string(serializedEventJson))) + return + } + // If User is not part of Navi Workspace Team, then we don't need to process the event + if userChangeEventRequest.User.Profile.TeamName != viper.GetString("slack.workspace.id") { + return + } + userEntity := user.UserEntity{ + Name: userChangeEventRequest.User.Name, + SlackUserId: userChangeEventRequest.User.ID, + Email: userChangeEventRequest.User.Profile.Email, + Image: userChangeEventRequest.User.Profile.Image, + RealName: userChangeEventRequest.User.Profile.RealName, + Active: !userChangeEventRequest.User.Deleted, + IsBot: userChangeEventRequest.User.IsBot, + } + existingUser, err := userChangeEventAction.userRepository.FindHoustonUserBySlackUserId(userEntity.SlackUserId) + if err != nil { + userChangeEventAction.logger.Error("error in finding user", zap.String("user_slack_id", userChangeEventRequest.User.ID), zap.Error(err)) + return + } + if existingUser == nil { + userChangeEventAction.logger.Info(fmt.Sprintf("inserting user %s", userEntity.Name)) + resultError := userChangeEventAction.userRepository.InsertHoustonUser(&userEntity) + if resultError != nil { + userChangeEventAction.logger.Error("error in inserting user", zap.String("user_slack_id", userChangeEventRequest.User.ID), zap.Error(resultError)) + return + } + return + } + // If User is Totally Deactivated then only we need to update the Active Field + if userEntity.RealName == util.DeactivatedUserName { + existingUser.Active = false + resultError := userChangeEventAction.userRepository.UpdateHoustonUser(*existingUser) + if resultError != nil { + userChangeEventAction.logger.Error("error in updating user", zap.String("user_slack_id", userChangeEventRequest.User.ID), zap.Error(resultError)) + return + } + return + } + + updatedExistingUser, isChanged := util.UpdateUserFieldsIfChanged(existingUser, &userEntity) + + if isChanged { + userChangeEventAction.logger.Info(fmt.Sprintf("upadting user %s", updatedExistingUser.Name)) + resultError := userChangeEventAction.userRepository.UpdateHoustonUser(*updatedExistingUser) + if resultError != nil { + userChangeEventAction.logger.Error("error in updating user", zap.String("user_slack_id", userChangeEventRequest.User.ID), zap.Error(resultError)) + } + } +} diff --git a/internal/processor/events_api_event_processor.go b/internal/processor/events_api_event_processor.go index d581bcb..8c8784c 100644 --- a/internal/processor/events_api_event_processor.go +++ b/internal/processor/events_api_event_processor.go @@ -2,14 +2,14 @@ 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/model/incident" "houston/model/severity" "houston/model/team" - - "github.com/slack-go/slack/slackevents" - "github.com/slack-go/slack/socketmode" - "go.uber.org/zap" + "houston/model/user" ) type eventsApiEventProcessor interface { @@ -42,3 +42,29 @@ func (mjc *MemberJoinedCallbackEventProcessor) ProcessCommand(event *slackevents var payload interface{} mjc.socketModeClient.Ack(*request, payload) } + +type UserChangeEventProcessor struct { + logger *zap.Logger + socketModeClient *socketmode.Client + userChangeAction *action.UserChangeEventAction +} + +func NewUserChangeEventProcessor(logger *zap.Logger, socketModeClient *socketmode.Client, userRepository *user.Repository) *UserChangeEventProcessor { + return &UserChangeEventProcessor{ + logger: logger, + socketModeClient: socketModeClient, + userChangeAction: action.NewUserChangeEventAction(logger, userRepository), + } +} + +func (ucep *UserChangeEventProcessor) ProcessCommand(event slackevents.EventsAPIEvent, request *socketmode.Request) { + defer func() { + if r := recover(); r != nil { + ucep.logger.Error(fmt.Sprintf("[UserChangeEventProcessor] Exception occurred: %v", r.(error))) + } + }() + + ucep.userChangeAction.PerformAction(event) + var payload interface{} + ucep.socketModeClient.Ack(*request, payload) +} diff --git a/model/user/model.go b/model/user/model.go index f91a62e..66fa4c8 100644 --- a/model/user/model.go +++ b/model/user/model.go @@ -7,6 +7,10 @@ type UserEntity struct { Name string SlackUserId string Active bool + IsBot bool + Email string + RealName string + Image string } func (UserEntity) TableName() string { diff --git a/model/user/user.go b/model/user/user.go index 783ebb8..15501e5 100644 --- a/model/user/user.go +++ b/model/user/user.go @@ -1,6 +1,7 @@ package user import ( + "errors" "go.uber.org/zap" "gorm.io/gorm" ) @@ -26,8 +27,8 @@ func (r *Repository) InsertHoustonUsers(users []UserEntity) error { return nil } -func (r *Repository) UpdateHoustonUsers(user UserEntity) error { - result := r.gormClient.Updates(&user) +func (r *Repository) UpdateHoustonUser(user UserEntity) error { + result := r.gormClient.Select("*").Updates(&user) if result.Error != nil || result.RowsAffected != 1 { return result.Error } @@ -56,6 +57,27 @@ func (r *Repository) IsAHoustonUser(nameOrSlackUserId string) (bool, *UserEntity return true, &existingUser } +func (r *Repository) FindHoustonUserBySlackUserId(slackUserId string) (*UserEntity, error) { + var user UserEntity + result := r.gormClient.Where("slack_user_id = ?", slackUserId).First(&user) + if result.Error != nil { + // If the user is not found, return both entity and error as nil + if errors.Is(gorm.ErrRecordNotFound, result.Error) { + return nil, nil + } + return nil, result.Error + } + return &user, nil +} + +func (r *Repository) InsertHoustonUser(user *UserEntity) error { + result := r.gormClient.Create(&user) + if result.Error != nil || result.RowsAffected != 1 { + return result.Error + } + return nil +} + func (r *Repository) GetHoustonUsersBySlackId(slackUserId []string) (*[]UserEntity, error) { var users []UserEntity result := r.gormClient.Where("slack_user_id IN ?", slackUserId).Find(&users) @@ -64,3 +86,13 @@ func (r *Repository) GetHoustonUsersBySlackId(slackUserId []string) (*[]UserEnti } return &users, nil } + +func (r *Repository) GetAllActiveHoustonUserBots() ([]UserEntity, error) { + var users []UserEntity + result := r.gormClient.Where("is_bot = ? AND active = ?", true, true).Find(&users) + + if result.Error != nil { + return nil, result.Error + } + return users, nil +} diff --git a/service/request/user_change_event_request.go b/service/request/user_change_event_request.go new file mode 100644 index 0000000..350a30d --- /dev/null +++ b/service/request/user_change_event_request.go @@ -0,0 +1,17 @@ +package service + +type UserChangeEventRequest struct { + User struct { + ID string `json:"id"` + Name string `json:"name"` + Deleted bool `json:"deleted"` + IsBot bool `json:"is_bot"` + + Profile struct { + Email string `json:"email"` + Image string `json:"image_32"` + RealName string `json:"real_name"` + TeamName string `json:"team"` + } `json:"profile"` + } `json:"user"` +} diff --git a/service/response/user_response.go b/service/response/user_response.go index 369e9e0..a9e7815 100644 --- a/service/response/user_response.go +++ b/service/response/user_response.go @@ -7,6 +7,6 @@ type ChannelMembersResponse struct { type UserResponse struct { Id string `json:"id"` Name string `json:"name"` - Email string `json:"email"` - Image string `json:"image"` + Email string `json:"email,omitempty"` + Image string `json:"image,omitempty"` } diff --git a/service/users_service.go b/service/users_service.go index 25be827..2958a8c 100644 --- a/service/users_service.go +++ b/service/users_service.go @@ -124,7 +124,7 @@ func (u *UserService) GetUsersInConversation(c *gin.Context) { func (u *UserService) UpdateHoustonUsers(c *gin.Context) { userEmail := c.GetHeader("X-User-Email") - authResult, _ := u.authService.checkIfManagerOrAdmin(c, "", Admin, Manager) + authResult, _ := u.authService.checkIfManagerOrAdmin(c, "", Admin) if !authResult { err := errors.New(fmt.Sprintf("%v is not an admin", userEmail)) c.JSON(http.StatusUnauthorized, common.ErrorResponse(err, http.StatusUnauthorized, nil)) @@ -140,3 +140,24 @@ func (u *UserService) UpdateHoustonUsers(c *gin.Context) { c.JSON(http.StatusOK, common.SuccessResponse("done", http.StatusOK)) } + +func (u *UserService) GetAllHoustonUserBots(c *gin.Context) { + userRepository := user.NewUserRepository(u.logger, u.db) + botUsers, err := userRepository.GetAllActiveHoustonUserBots() + if err != nil { + u.logger.Error("error in getting all houston_user bots", zap.Error(err)) + c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil)) + return + } + userResponses := make([]service.UserResponse, 0, len(botUsers)) + + for _, botUser := range botUsers { + userResponse := service.UserResponse{ + Id: botUser.SlackUserId, + Name: botUser.RealName, + } + userResponses = append(userResponses, userResponse) + } + c.JSON(http.StatusOK, common.SuccessResponse(userResponses, http.StatusOK)) + +}