diff --git a/go.mod b/go.mod index b8461cd55d2c97ac440dcd625e4b58039f80d775..1b73d8a9122c7b78f46c5fbecc29e46910167b76 100644 --- a/go.mod +++ b/go.mod @@ -64,6 +64,7 @@ require ( github.com/go-pg/zerochecker v0.2.0 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/gorilla/websocket v1.4.2 // indirect github.com/hbollon/go-edlib v1.6.0 // indirect github.com/inconshreveable/log15 v3.0.0-testing.3+incompatible // indirect github.com/inconshreveable/log15/v3 v3.0.0-testing.5 // indirect @@ -76,6 +77,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/rivo/uniseg v0.1.0 // indirect + github.com/slack-go/slack v0.15.0 // indirect github.com/smartystreets/goconvey v1.7.2 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect diff --git a/go.sum b/go.sum index cb29536ba98516e6d496bdc8d9ad14ac2fb890c0..286892cadb8eb13b942594b3d00d2ca65de08c97 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,7 @@ github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSM github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -131,6 +132,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -138,6 +140,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= @@ -223,6 +227,8 @@ github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/slack-go/slack v0.15.0 h1:LE2lj2y9vqqiOf+qIIy0GvEoxgF1N5yLGZffmEZykt0= +github.com/slack-go/slack v0.15.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= diff --git a/slack_utils/slack_utils.go b/slack_utils/slack_utils.go new file mode 100644 index 0000000000000000000000000000000000000000..1d35bc19786454f2fb49995eea71d867a62ecf9e --- /dev/null +++ b/slack_utils/slack_utils.go @@ -0,0 +1,130 @@ +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 +}