package slack import ( "context" "encoding/json" "errors" "net/url" "strings" ) const ( DEFAULT_MESSAGE_USERNAME = "" DEFAULT_MESSAGE_THREAD_TIMESTAMP = "" DEFAULT_MESSAGE_REPLY_BROADCAST = false DEFAULT_MESSAGE_ASUSER = false DEFAULT_MESSAGE_PARSE = "" DEFAULT_MESSAGE_LINK_NAMES = 0 DEFAULT_MESSAGE_UNFURL_LINKS = false DEFAULT_MESSAGE_UNFURL_MEDIA = true DEFAULT_MESSAGE_ICON_URL = "" DEFAULT_MESSAGE_ICON_EMOJI = "" DEFAULT_MESSAGE_MARKDOWN = true DEFAULT_MESSAGE_ESCAPE_TEXT = true ) type chatResponseFull struct { Channel string `json:"channel"` Timestamp string `json:"ts"` Text string `json:"text"` SlackResponse } // PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request type PostMessageParameters struct { Text string `json:"text"` Username string `json:"user_name"` AsUser bool `json:"as_user"` Parse string `json:"parse"` ThreadTimestamp string `json:"thread_ts"` ReplyBroadcast bool `json:"reply_broadcast"` LinkNames int `json:"link_names"` Attachments []Attachment `json:"attachments"` UnfurlLinks bool `json:"unfurl_links"` UnfurlMedia bool `json:"unfurl_media"` IconURL string `json:"icon_url"` IconEmoji string `json:"icon_emoji"` Markdown bool `json:"mrkdwn,omitempty"` EscapeText bool `json:"escape_text"` // chat.postEphemeral support Channel string `json:"channel"` User string `json:"user"` } // NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set func NewPostMessageParameters() PostMessageParameters { return PostMessageParameters{ Username: DEFAULT_MESSAGE_USERNAME, User: DEFAULT_MESSAGE_USERNAME, AsUser: DEFAULT_MESSAGE_ASUSER, Parse: DEFAULT_MESSAGE_PARSE, LinkNames: DEFAULT_MESSAGE_LINK_NAMES, Attachments: nil, UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS, UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA, IconURL: DEFAULT_MESSAGE_ICON_URL, IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI, Markdown: DEFAULT_MESSAGE_MARKDOWN, EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT, } } // DeleteMessage deletes a message in a channel func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext(context.Background(), channel, MsgOptionDelete(messageTimestamp)) return respChannel, respTimestamp, err } // DeleteMessageContext deletes a message in a channel with a custom context func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTimestamp string) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext(ctx, channel, MsgOptionDelete(messageTimestamp)) return respChannel, respTimestamp, err } // PostMessage sends a message to a channel. // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. func (api *Client) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext( context.Background(), channel, MsgOptionText(text, params.EscapeText), MsgOptionAttachments(params.Attachments...), MsgOptionPostMessageParameters(params), ) return respChannel, respTimestamp, err } // PostMessageContext sends a message to a channel with a custom context // For more details, see PostMessage documentation func (api *Client) PostMessageContext(ctx context.Context, channel, text string, params PostMessageParameters) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext( ctx, channel, MsgOptionText(text, params.EscapeText), MsgOptionAttachments(params.Attachments...), MsgOptionPostMessageParameters(params), ) return respChannel, respTimestamp, err } // PostEphemeral sends an ephemeral message to a user in a channel. // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. func (api *Client) PostEphemeral(channel, userID string, options ...MsgOption) (string, error) { options = append(options, MsgOptionPostEphemeral()) return api.PostEphemeralContext( context.Background(), channel, userID, options..., ) } // PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context // For more details, see PostEphemeral documentation func (api *Client) PostEphemeralContext(ctx context.Context, channel, userID string, options ...MsgOption) (string, error) { path, values, err := ApplyMsgOptions(api.config.token, channel, options...) if err != nil { return "", err } values.Add("user", userID) response, err := chatRequest(ctx, path, values, api.debug) if err != nil { return "", err } return response.Timestamp, nil } // UpdateMessage updates a message in a channel func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) { return api.UpdateMessageContext(context.Background(), channel, timestamp, text) } // UpdateMessage updates a message in a channel func (api *Client) UpdateMessageContext(ctx context.Context, channel, timestamp, text string) (string, string, string, error) { return api.SendMessageContext(ctx, channel, MsgOptionUpdate(timestamp), MsgOptionText(text, true)) } // SendMessage more flexible method for configuring messages. func (api *Client) SendMessage(channel string, options ...MsgOption) (string, string, string, error) { return api.SendMessageContext(context.Background(), channel, options...) } // SendMessageContext more flexible method for configuring messages with a custom context. func (api *Client) SendMessageContext(ctx context.Context, channel string, options ...MsgOption) (string, string, string, error) { channel, values, err := ApplyMsgOptions(api.config.token, channel, options...) if err != nil { return "", "", "", err } response, err := chatRequest(ctx, channel, values, api.debug) if err != nil { return "", "", "", err } return response.Channel, response.Timestamp, response.Text, nil } // ApplyMsgOptions utility function for debugging/testing chat requests. func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) { config := sendConfig{ mode: chatPostMessage, values: url.Values{ "token": {token}, "channel": {channel}, }, } for _, opt := range options { if err := opt(&config); err != nil { return string(config.mode), config.values, err } } return string(config.mode), config.values, nil } func escapeMessage(message string) string { replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") return replacer.Replace(message) } func chatRequest(ctx context.Context, path string, values url.Values, debug bool) (*chatResponseFull, error) { response := &chatResponseFull{} err := post(ctx, path, values, response, debug) if err != nil { return nil, err } if !response.Ok { return nil, errors.New(response.Error) } return response, nil } type sendMode string const ( chatUpdate sendMode = "chat.update" chatPostMessage sendMode = "chat.postMessage" chatDelete sendMode = "chat.delete" chatPostEphemeral sendMode = "chat.postEphemeral" ) type sendConfig struct { mode sendMode values url.Values } // MsgOption option provided when sending a message. type MsgOption func(*sendConfig) error // MsgOptionPost posts a messages, this is the default. func MsgOptionPost() MsgOption { return func(config *sendConfig) error { config.mode = chatPostMessage config.values.Del("ts") return nil } } // MsgOptionPostEphemeral posts an ephemeral message func MsgOptionPostEphemeral() MsgOption { return func(config *sendConfig) error { config.mode = chatPostEphemeral config.values.Del("ts") return nil } } // MsgOptionUpdate updates a message based on the timestamp. func MsgOptionUpdate(timestamp string) MsgOption { return func(config *sendConfig) error { config.mode = chatUpdate config.values.Add("ts", timestamp) return nil } } // MsgOptionDelete deletes a message based on the timestamp. func MsgOptionDelete(timestamp string) MsgOption { return func(config *sendConfig) error { config.mode = chatDelete config.values.Add("ts", timestamp) return nil } } // MsgOptionAsUser whether or not to send the message as the user. func MsgOptionAsUser(b bool) MsgOption { return func(config *sendConfig) error { if b != DEFAULT_MESSAGE_ASUSER { config.values.Set("as_user", "true") } return nil } } // MsgOptionText provide the text for the message, optionally escape the provided // text. func MsgOptionText(text string, escape bool) MsgOption { return func(config *sendConfig) error { if escape { text = escapeMessage(text) } config.values.Add("text", text) return nil } } // MsgOptionAttachments provide attachments for the message. func MsgOptionAttachments(attachments ...Attachment) MsgOption { return func(config *sendConfig) error { if attachments == nil { return nil } attachments, err := json.Marshal(attachments) if err == nil { config.values.Set("attachments", string(attachments)) } return err } } // MsgOptionEnableLinkUnfurl enables link unfurling func MsgOptionEnableLinkUnfurl() MsgOption { return func(config *sendConfig) error { config.values.Set("unfurl_links", "true") return nil } } // MsgOptionDisableMediaUnfurl disables media unfurling. func MsgOptionDisableMediaUnfurl() MsgOption { return func(config *sendConfig) error { config.values.Set("unfurl_media", "false") return nil } } // MsgOptionDisableMarkdown disables markdown. func MsgOptionDisableMarkdown() MsgOption { return func(config *sendConfig) error { config.values.Set("mrkdwn", "false") return nil } } // MsgOptionPostMessageParameters maintain backwards compatibility. func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { return func(config *sendConfig) error { if params.Username != DEFAULT_MESSAGE_USERNAME { config.values.Set("username", string(params.Username)) } // chat.postEphemeral support if params.User != DEFAULT_MESSAGE_USERNAME { config.values.Set("user", params.User) } // never generates an error. MsgOptionAsUser(params.AsUser)(config) if params.Parse != DEFAULT_MESSAGE_PARSE { config.values.Set("parse", string(params.Parse)) } if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { config.values.Set("link_names", "1") } if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS { config.values.Set("unfurl_links", "true") } // I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request. // Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side. if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS { config.values.Set("unfurl_links", "false") } if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA { config.values.Set("unfurl_media", "false") } if params.IconURL != DEFAULT_MESSAGE_ICON_URL { config.values.Set("icon_url", params.IconURL) } if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI { config.values.Set("icon_emoji", params.IconEmoji) } if params.Markdown != DEFAULT_MESSAGE_MARKDOWN { config.values.Set("mrkdwn", "false") } if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP { config.values.Set("thread_ts", params.ThreadTimestamp) } if params.ReplyBroadcast != DEFAULT_MESSAGE_REPLY_BROADCAST { config.values.Set("reply_broadcast", "true") } return nil } }