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  }