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
This commit is contained in:
Gullipalli Chetan Kumar
2023-10-20 13:05:40 +05:30
committed by GitHub
parent aef97d4c6e
commit 6b44ec9ec3
13 changed files with 281 additions and 30 deletions

View File

@@ -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)
}
}
}

View File

@@ -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)

View File

@@ -36,3 +36,8 @@ const (
ShowIncidentSubmit = "show_incident_submit"
MarkIncidentDuplicateSubmit = "mark_incident_duplicate_submit"
)
const (
UserChangeEvent = "user_change"
DeactivatedUserName = "Deactivated User"
)

View File

@@ -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
}

View File

@@ -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
create-incident-v2-enabled=CREATE_INCIDENT_V2_ENABLED
#slack details
slack.workspace.id=SLACK_WORKSPACE_ID

View File

@@ -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))
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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))
}