github.com/mongodb/grip@v0.0.0-20240213223901-f906268d82b9/message/slack.go (about) 1 package message 2 3 import ( 4 "errors" 5 "fmt" 6 "reflect" 7 8 "github.com/mongodb/grip/level" 9 "github.com/slack-go/slack" 10 ) 11 12 const ( 13 // slackMaxAttachments is the maximum number of attachments a single 14 // Slack message may have, per the Slack API documentation: 15 // https://api.slack.com/docs/message-attachments#attachment_limits 16 slackMaxAttachments = 100 17 ) 18 19 // Slack is a message to a Slack channel or user 20 type Slack struct { 21 Target string `bson:"target" json:"target" yaml:"target"` 22 Msg string `bson:"msg" json:"msg" yaml:"msg"` 23 Attachments []slack.Attachment `bson:"attachments" json:"attachments" yaml:"attachments"` 24 } 25 26 // SlackAttachment is a single attachment to a slack message. 27 // This type contains fields found in slack-go/slack.Attachment 28 type SlackAttachment struct { 29 Color string `bson:"color,omitempty" json:"color,omitempty" yaml:"color,omitempty"` 30 Fallback string `bson:"fallback,omitempty" json:"fallback,omitempty" yaml:"fallback,omitempty"` 31 32 AuthorName string `bson:"author_name,omitempty" json:"author_name,omitempty" yaml:"author_name,omitempty"` 33 AuthorIcon string `bson:"author_icon,omitempty" json:"author_icon,omitempty" yaml:"author_icon,omitempty"` 34 35 Title string `bson:"title,omitempty" json:"title,omitempty" yaml:"title,omitempty"` 36 TitleLink string `bson:"title_link,omitempty" json:"title_link,omitempty" yaml:"title_link,omitempty"` 37 Text string `bson:"text,omitempty" json:"text,omitempty" yaml:"text,omitempty"` 38 39 Fields []*SlackAttachmentField `bson:"fields,omitempty" json:"fields,omitempty" yaml:"fields,omitempty"` 40 MarkdownIn []string `bson:"mrkdwn_in,omitempty" json:"mrkdwn_in,omitempty" yaml:"mrkdwn_in,omitempty"` 41 42 Footer string `bson:"footer,omitempty" json:"footer,omitempty" yaml:"footer,omitempty"` 43 } 44 45 func (s *SlackAttachment) convert() *slack.Attachment { 46 const skipField = "Fields" 47 at := slack.Attachment{} 48 49 vGrip := reflect.ValueOf(s).Elem() 50 tGrip := reflect.TypeOf(s).Elem() 51 vSlack := reflect.ValueOf(&at).Elem() 52 for fNum := 0; fNum < vGrip.NumField(); fNum++ { 53 gripField := vGrip.Field(fNum) 54 gripFieldName := tGrip.Field(fNum).Name 55 slackField := vSlack.FieldByName(gripFieldName) 56 if gripFieldName != skipField { 57 slackField.Set(gripField) 58 59 } else { 60 at.Fields = make([]slack.AttachmentField, 0, len(s.Fields)) 61 62 for i := range s.Fields { 63 at.Fields = append(at.Fields, *s.Fields[i].convert()) 64 } 65 } 66 } 67 68 return &at 69 } 70 71 // SlackAttachmentField is one of the optional fields that can be attached 72 // to a slack message. This type is the same as slack-go/slack.AttachmentField 73 type SlackAttachmentField struct { 74 Title string `bson:"title" json:"title" yaml:"title"` 75 Value string `bson:"value" json:"value" yaml:"value"` 76 Short bool `bson:"short" json:"short" yaml:"short"` 77 } 78 79 func (s *SlackAttachmentField) convert() *slack.AttachmentField { 80 af := slack.AttachmentField{} 81 82 vGrip := reflect.ValueOf(s).Elem() 83 tGrip := reflect.TypeOf(s).Elem() 84 vSlack := reflect.ValueOf(&af).Elem() 85 for fNum := 0; fNum < vGrip.NumField(); fNum++ { 86 gripField := vGrip.Field(fNum) 87 gripFieldName := tGrip.Field(fNum).Name 88 slackField := vSlack.FieldByName(gripFieldName) 89 slackField.Set(gripField) 90 } 91 92 return &af 93 } 94 95 type slackMessage struct { 96 raw Slack 97 98 Base `bson:"metadata" json:"metadata" yaml:"metadata"` 99 } 100 101 // NewSlackMessage creates a composer for messages to slack 102 func NewSlackMessage(p level.Priority, target string, msg string, attachments []SlackAttachment) Composer { 103 s := MakeSlackMessage(target, msg, attachments) 104 _ = s.SetPriority(p) 105 106 return s 107 } 108 109 // MakeSlackMessage creates a composer for message to slack without a priority 110 func MakeSlackMessage(target string, msg string, attachments []SlackAttachment) Composer { 111 s := &slackMessage{ 112 raw: Slack{ 113 Target: target, 114 Msg: msg, 115 }, 116 } 117 if len(attachments) != 0 { 118 s.raw.Attachments = make([]slack.Attachment, 0, len(attachments)) 119 120 for i := range attachments { 121 at := attachments[i].convert() 122 s.raw.Attachments = append(s.raw.Attachments, *at) 123 } 124 } 125 126 return s 127 } 128 129 func (c *slackMessage) Loggable() bool { 130 if len(c.raw.Target) == 0 { 131 return false 132 } 133 if len(c.raw.Msg) == 0 { 134 return false 135 } 136 if len(c.raw.Attachments) > slackMaxAttachments { 137 return false 138 } 139 140 return true 141 } 142 143 func (c *slackMessage) String() string { 144 return fmt.Sprintf("%s: %s", c.raw.Target, c.raw.Msg) 145 } 146 147 func (c *slackMessage) Raw() interface{} { 148 return &c.raw 149 } 150 151 // Annotate adds additional attachments to the message. The key value is ignored 152 // if a SlackAttachment or *SlackAttachment is supplied 153 func (c *slackMessage) Annotate(key string, data interface{}) error { 154 var annotate *SlackAttachment 155 156 switch v := data.(type) { 157 case *SlackAttachment: 158 annotate = v 159 case SlackAttachment: 160 annotate = &v 161 default: 162 return c.Base.Annotate(key, data) 163 } 164 if annotate == nil { 165 return errors.New("annotate data must not be nil") 166 } 167 if len(c.raw.Attachments) == slackMaxAttachments { 168 return fmt.Errorf("adding another Slack attachment would exceed maximum number of attachments, %d", slackMaxAttachments) 169 } 170 171 c.raw.Attachments = append(c.raw.Attachments, *annotate.convert()) 172 173 return nil 174 }