github.com/sharovik/devbot@v1.0.1-0.20240308094637-4a0387c40516/internal/service/message/slack_service.go (about) 1 package message 2 3 import ( 4 "crypto/tls" 5 "encoding/json" 6 "errors" 7 "net/http" 8 "regexp" 9 "strings" 10 "time" 11 12 "github.com/sharovik/devbot/internal/service/message/conversation" 13 14 "github.com/sharovik/devbot/internal/client" 15 "github.com/sharovik/devbot/internal/config" 16 "github.com/sharovik/devbot/internal/container" 17 "github.com/sharovik/devbot/internal/dto" 18 "github.com/sharovik/devbot/internal/log" 19 "github.com/sharovik/devbot/internal/service/analiser" 20 "golang.org/x/net/websocket" 21 ) 22 23 // MsgAttributes the validation message attributes 24 type MsgAttributes struct { 25 Type string 26 Channel string 27 Text string 28 User string 29 BotID string 30 } 31 32 const ( 33 slackAPIOrigin = "https://slack.com/api" 34 35 eventTypeMessage = "message" 36 eventTypeDesktopNotification = "desktop_notification" 37 eventTypeFileShared = "file_shared" 38 eventTypeAppMention = "app_mention" 39 ) 40 41 var ( 42 acceptedMessageTypes = map[string]string{ 43 eventTypeMessage: eventTypeMessage, 44 eventTypeDesktopNotification: eventTypeDesktopNotification, 45 eventTypeFileShared: eventTypeFileShared, 46 eventTypeAppMention: eventTypeAppMention, 47 } 48 ) 49 50 // SlackService struct of message service 51 type SlackService struct { 52 } 53 54 func (SlackService) fetchMainChannelID() error { 55 availableChannels, statusCode, err := container.C.MessageClient.GetConversationsList() 56 if err != nil { 57 log.Logger().AddError(err).Int("status_code", statusCode).Msg("Failed conversations list fetching") 58 return err 59 } 60 61 var generalChannel dto.Channel 62 for _, channel := range availableChannels.Channels { 63 if channel.Name == container.C.Config.MessagesAPIConfig.MainChannelAlias { 64 generalChannel = channel 65 break 66 } 67 } 68 69 if container.C.Config.MessagesAPIConfig.MainChannelID == "" { 70 if err := container.C.Config.SetToEnv(config.EnvMainChannelID, generalChannel.ID, true); err != nil { 71 log.Logger().AddError(err).Str("channel_id", generalChannel.ID).Msg("Failed to save slackEnvMainChannelID in .env file") 72 return err 73 } 74 75 container.C.Config.MessagesAPIConfig.MainChannelID = generalChannel.ID 76 } 77 78 return nil 79 } 80 81 func (SlackService) fetchBotUserID() error { 82 availableUsers, statusCode, err := container.C.MessageClient.GetUsersList() 83 if err != nil { 84 log.Logger().AddError(err).Int("status_code", statusCode).Msg("Failed conversations list fetching") 85 return err 86 } 87 88 var botMember dto.SlackMember 89 for _, member := range availableUsers.Members { 90 if member.Profile.RealName == container.C.Config.MessagesAPIConfig.BotName { 91 botMember = member 92 break 93 } 94 } 95 96 if container.C.Config.MessagesAPIConfig.BotUserID == "" { 97 if err := container.C.Config.SetToEnv(config.EnvUserID, botMember.ID, true); err != nil { 98 log.Logger().AddError(err).Str("user_id", botMember.ID).Msg("Failed to save slackEnvMainChannelID in .env file") 99 return err 100 } 101 102 container.C.Config.MessagesAPIConfig.BotUserID = botMember.ID 103 } 104 105 return nil 106 } 107 108 // BeforeWSConnectionStart runs methods before the WS connection start 109 func (s SlackService) BeforeWSConnectionStart() error { 110 if container.C.Config.MessagesAPIConfig.MainChannelID == "" { 111 log.Logger().Info().Msg("Main channel ID wasn't specified. Trying to fetch main channel from API") 112 if err := s.fetchMainChannelID(); err != nil { 113 log.Logger().AddError(err).Msg("Failed to fetch channels") 114 return err 115 } 116 } 117 118 if container.C.Config.MessagesAPIConfig.BotUserID == "" { 119 log.Logger().Info().Msg("Bot user ID wasn't specified. Trying to fetch user ID from API") 120 if err := s.fetchBotUserID(); err != nil { 121 log.Logger().AddError(err).Msg("Failed to fetch user ID") 122 return err 123 } 124 } 125 126 log.Logger().AppendGlobalContext(map[string]interface{}{ 127 "main_channel_id": container.C.Config.MessagesAPIConfig.MainChannelID, 128 "main_channel_alias": container.C.Config.MessagesAPIConfig.MainChannelAlias, 129 "bot_user_id": container.C.Config.MessagesAPIConfig.BotUserID, 130 "bot_user_name": container.C.Config.MessagesAPIConfig.BotName, 131 }) 132 133 return nil 134 } 135 136 // InitWebSocketReceiver method for initialization of websocket receiver 137 func (s SlackService) InitWebSocketReceiver() error { 138 if err := s.BeforeWSConnectionStart(); err != nil { 139 log.Logger().AddError(err).Msg("Failed to prepare service for WS connection") 140 return err 141 } 142 143 ws, statusCode, err := s.wsConnect() 144 if err != nil { 145 log.Logger().AddError(err).Int("status_code", statusCode).Msg("Failed connect to the websocket") 146 return err 147 } 148 149 var ( 150 event interface{} 151 ) 152 153 for { 154 var message dto.SlackResponseEventAPIMessage 155 156 //Receive message 157 if err = websocket.JSON.Receive(ws, &event); err != nil { 158 log.Logger().AddError(err).Msg("Something went wrong with message receiving from EventsAPI") 159 return err 160 } 161 162 str, _ := json.Marshal(&event) 163 if strings.Contains(string(str), `"channel":{"created"`) || strings.Contains(string(str), `"type":"user_change"`) { 164 log.Logger().Debug().RawJSON("message_body", str).Msg("Received not supported event. Ignoring.") 165 continue 166 } 167 168 if err = json.Unmarshal(str, &message); err != nil { 169 log.Logger().AddError(err). 170 RawJSON("message_body", str). 171 Msg("Something went wrong with message parsing") 172 return err 173 } 174 175 if message.Type == "hello" { 176 continue 177 } 178 179 log.Logger().Debug(). 180 RawJSON("message_body", str). 181 Str("envelope_id", message.EnvelopeID). 182 Msg("Received event message") 183 184 if err = acknowledge(ws, message.EnvelopeID); err != nil { 185 log.Logger().AddError(err). 186 RawJSON("message_body", str). 187 Str("envelope_id", message.EnvelopeID). 188 Msg("Failed to acknowledge the message") 189 190 return err 191 } 192 193 if !isValidMessage(MsgAttributes{ 194 Type: message.Payload.Event.Type, 195 Channel: message.Payload.Event.Channel, 196 Text: message.Payload.Event.Text, 197 User: message.Payload.Event.User, 198 BotID: message.Payload.Event.BotID, 199 }) { 200 continue 201 } 202 203 if err = s.ProcessMessage(&message); err != nil { 204 log.Logger().AddError(err).Interface("message_object", &message).Msg("Can't check or answer to the message") 205 } 206 207 conversation.CleanUpExpiredMessages() 208 } 209 } 210 211 func acknowledge(ws *websocket.Conn, envelopeID string) error { 212 res := dto.SlackRequestAcknowledge{ 213 EnvelopeID: envelopeID, 214 } 215 216 log.Logger().Debug(). 217 Str("envelope_id", envelopeID). 218 Msg("Acknowledge event message") 219 220 return websocket.JSON.Send(ws, res) 221 } 222 223 // ProcessMessage processes the message from the WS connection 224 func (s SlackService) ProcessMessage(msg interface{}) error { 225 message := msg.(*dto.SlackResponseEventAPIMessage) 226 log.Logger().Debug(). 227 Str("type", message.Type). 228 Str("text", message.Payload.Event.Text). 229 Str("team", message.Payload.Event.Team). 230 Str("ts", message.Payload.Event.Ts). 231 Str("user", message.Payload.Event.User). 232 Str("channel", message.Payload.Event.Channel). 233 Msg("Message received") 234 235 //We need to trim the message before all checks 236 message.Payload.Event.Text = strings.TrimSpace(message.Payload.Event.Text) 237 238 dmAnswer, err := analiser.GetDmAnswer(analiser.Message{ 239 Channel: message.Payload.Event.Channel, 240 User: message.Payload.Event.User, 241 Text: message.Payload.Event.Text, 242 }) 243 if err != nil { 244 log.Logger().AddError(err).Msg("Failed to get dictionary message answer") 245 return err 246 } 247 248 m, err := prepareAnswer(&dto.SlackResponseEventMessage{ 249 Channel: message.Payload.Event.Channel, 250 ClientMsgID: message.Payload.Event.ClientMsgID, 251 DisplayAsBot: false, 252 EventTs: message.Payload.Event.EventTs, 253 ThreadTS: message.Payload.Event.ThreadTS, 254 Files: nil, 255 Team: message.Payload.Event.Team, 256 Text: message.Payload.Event.Text, 257 Ts: message.Payload.Event.Ts, 258 Type: message.Payload.Event.Type, 259 User: message.Payload.Event.User, 260 }, dmAnswer) 261 if err != nil { 262 log.Logger().AddError(err).Msg("Failed to analyse received message") 263 return err 264 } 265 266 emptyDmMessage := dto.DictionaryMessage{} 267 if dmAnswer == emptyDmMessage { 268 log.Logger().Debug(). 269 Str("type", message.Payload.Event.Type). 270 Str("text", message.Payload.Event.Text). 271 Str("team", message.Payload.Event.Team). 272 Str("ts", message.Payload.Event.Ts). 273 Str("user", message.Payload.Event.User). 274 Str("channel", message.Payload.Event.Channel). 275 Msg("No answer found for the received message") 276 } else { 277 //We put a dictionary message into our message object, 278 // so later we can identify what kind of reaction will be executed 279 m.DictionaryMessage = dmAnswer 280 } 281 282 if err = TriggerAnswer(message.Payload.Event.Channel, m, true); err != nil { 283 log.Logger().AddError(err).Msg("Failed trigger the answer") 284 return err 285 } 286 287 refreshPreparedMessages() 288 return nil 289 } 290 291 func getWSClient() client.SlackClient { 292 netTransport := &http.Transport{ 293 TLSHandshakeTimeout: time.Duration(container.C.Config.HTTPClient.TLSHandshakeTimeout) * time.Second, 294 TLSClientConfig: &tls.Config{ 295 InsecureSkipVerify: container.C.Config.HTTPClient.InsecureSkipVerify, 296 }, 297 } 298 299 httpClient := http.Client{ 300 Timeout: time.Duration(container.C.Config.HTTPClient.RequestTimeout) * time.Second, 301 Transport: netTransport, 302 } 303 304 c := client.HTTPClient{ 305 Client: &httpClient, 306 } 307 308 c.SetOauthToken(container.C.Config.MessagesAPIConfig.OAuthToken) 309 c.SetBaseURL(container.C.Config.MessagesAPIConfig.BaseURL) 310 311 sc := client.SlackClient{} 312 sc.HTTPClient = &c 313 314 return sc 315 } 316 317 // wsConnect method for receiving of websocket URL which we will use for our connection 318 func (SlackService) wsConnect() (*websocket.Conn, int, error) { 319 response, statusCode, err := getWSClient().HTTPClient.Post("/apps.connections.open", []byte{}, map[string]string{}) 320 if err != nil { 321 log.Logger().AddError(err).RawJSON("response", response).Int("status_code", statusCode).Msg("Failed send message") 322 return &websocket.Conn{}, statusCode, err 323 } 324 325 var dtoResponse dto.SlackResponseRTMConnect 326 if err := json.Unmarshal(response, &dtoResponse); err != nil { 327 return &websocket.Conn{}, statusCode, err 328 } 329 330 if !dtoResponse.Ok { 331 return &websocket.Conn{}, statusCode, errors.New(dtoResponse.Error) 332 } 333 334 ws, err := websocket.Dial(dtoResponse.URL, "", slackAPIOrigin) 335 336 return ws, statusCode, err 337 } 338 339 func isValidMessage(msg MsgAttributes) bool { 340 if acceptedMessageTypes[msg.Type] == "" { 341 log.Logger().Debug().Str("type", msg.Type).Msg("Skip message check for this message type") 342 return false 343 } 344 345 if msg.Channel == "" { 346 log.Logger().Debug().Msg("Message channel cannot be empty") 347 return false 348 } 349 350 if isGlobalAlertTriggered(msg.Text) { 351 log.Logger().Debug().Msg("The global alert is triggered. Skipping.") 352 return false 353 } 354 355 if msg.User == container.C.Config.MessagesAPIConfig.BotUserID || msg.BotID != "" { 356 log.Logger().Debug().Msg("This message is from our bot user") 357 return false 358 } 359 360 if msg.Text == "" { 361 log.Logger().Debug().Msg("This message has empty text. Skipping.") 362 return false 363 } 364 365 return true 366 } 367 368 func isGlobalAlertTriggered(text string) bool { 369 re, err := regexp.Compile(`(?i)(<!(here|channel)>)`) 370 if err != nil { 371 log.Logger().AddError(err).Msg("Failed to parse global alert text part") 372 return false 373 } 374 375 return re.MatchString(text) 376 }