package slack_utils import ( "fmt" "github.com/samber/lo" "github.com/slack-go/slack" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/logs" "reflect" "regexp" ) type SlackField struct { Label string Value any } type SlackFile struct { File string FileSize int Content string Filename string Title string InitialComment string ThreadTimestamp string AltTxt string SnippetText string } type SlackClient struct { Client *slack.Client } func GetClient(apiKey string) *SlackClient { return &SlackClient{ Client: slack.New(apiKey), } } func (s *SlackClient) SendAlert(message string, channel string, parentMessageTimestamp ...string) string { if s == nil || s.Client == nil { // If the slack client isn't set, log the message to Raygun instead logs.ErrorMsg(message) return "" } slackMessageOptions := []slack.MsgOption{ slack.MsgOptionText(message, false), } if len(parentMessageTimestamp) > 0 { slackMessageOptions = append(slackMessageOptions, slack.MsgOptionTS(parentMessageTimestamp[0])) } return s.postMessage(channel, slackMessageOptions) } func (s *SlackClient) SendAlertWithFields(message string, channel string, slackFields []SlackField, parentMessageTimestamp ...string) string { if s == nil || s.Client == nil { // If the slack client isn't set, log the message to Raygun instead slackFieldMap := slackFieldsToMap(slackFields) logs.ErrorWithFields(errors.Error(message), slackFieldMap) return "" } slackFieldsPerMessage := lo.Chunk(slackFields, 50) // Slack has a limit of 50 blocks per message var messageTimestamp string for i, slackFieldsForMessage := range slackFieldsPerMessage { messageText := message if len(slackFieldsPerMessage) > 1 { messageText = fmt.Sprintf("%s (%d/%d)", message, i+1, len(slackFieldsPerMessage)) } blocks := []slack.Block{slack.NewSectionBlock(slack.NewTextBlockObject( "plain_text", messageText, hasEmoji(messageText), false), nil, nil, ), } slackFieldsChunked := lo.Chunk(slackFieldsForMessage, 2) for _, slackFieldsChunk := range slackFieldsChunked { fieldBlocksForChunk := []*slack.TextBlockObject{} for _, slackField := range slackFieldsChunk { slackFieldValue := slackField.Value // if slackField.Value is a pointer, dereference it if reflect.ValueOf(slackField.Value).Kind() == reflect.Ptr { slackFieldValue = reflect.ValueOf(slackField.Value).Elem().Interface() } text := fmt.Sprintf("*%s*\n%v", slackField.Label, slackFieldValue) fieldBlocksForChunk = append(fieldBlocksForChunk, slack.NewTextBlockObject( "mrkdwn", text, false, false, )) } blocks = append(blocks, slack.NewSectionBlock(nil, fieldBlocksForChunk, nil)) } slackMessageOptions := []slack.MsgOption{ slack.MsgOptionBlocks(blocks...), } if len(parentMessageTimestamp) > 0 { slackMessageOptions = append(slackMessageOptions, slack.MsgOptionTS(parentMessageTimestamp[0])) } messageTimestamp = s.postMessage(channel, slackMessageOptions) } // Return the last message timestamp return messageTimestamp } func (s *SlackClient) postMessage(channel string, messageOptions []slack.MsgOption) string { _, messageTimestamp, err := s.Client.PostMessage(channel, messageOptions...) if err != nil { logs.ErrorWithMsg(err, "Error sending slack:") } return messageTimestamp } func (s *SlackClient) SendFile(channel string, file SlackFile) error { fileParameters := slack.UploadFileV2Parameters{ Channel: channel, File: file.File, FileSize: file.FileSize, Content: file.Content, Filename: file.Filename, Title: file.Title, InitialComment: file.InitialComment, ThreadTimestamp: file.ThreadTimestamp, AltTxt: file.AltTxt, SnippetText: file.SnippetText, } _, err := s.Client.UploadFileV2(fileParameters) if err != nil { logs.ErrorWithMsg(err, "Error uploading file to slack:") return err } return nil } func hasEmoji(message string) bool { emojiRegex := regexp.MustCompile(`:[a-zA-Z0-9_]+:`) return emojiRegex.MatchString(message) } func slackFieldsToMap(fields []SlackField) map[string]any { slackFieldsMap := make(map[string]any) for _, field := range fields { slackFieldsMap[field.Label] = field.Value } return slackFieldsMap }