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 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(slackFieldMap, errors.Error(message)) 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("Error sending slack: %v+\n", err) } return messageTimestamp } 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 }