diff --git a/README.md b/README.md index 03298c622c..5277a06e10 100644 --- a/README.md +++ b/README.md @@ -300,13 +300,14 @@ enable=true ```toml [slack] [slack.test] -Token="yourslacktoken" -PrefixMessagesWithNick=true +Token="xoxb-yourslackbottoken" +AppToken="xapp-yourapptoken" [discord] [discord.test] Token="yourdiscordtoken" Server="yourdiscordservername" +AutoWebhooks=true [general] RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> " @@ -324,6 +325,52 @@ RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> " channel = "general" ``` +> **Setup requirements for Slack and Discord are detailed below.** Both platforms have tightened their API access in recent years, so your bot apps need specific scopes and intents configured before matterbridge can function. + +#### Slack app setup + +Matterbridge uses **Socket Mode** to connect to Slack. You need both a Bot Token (`xoxb-`) and an App-Level Token (`xapp-`). + +1. Create a Slack app at +2. Enable **Socket Mode** under *Settings > Socket Mode* +3. Create an **App-Level Token** with the `connections:write` scope — this is your `AppToken` +4. Under *OAuth & Permissions*, add these **Bot Token Scopes**: + +| Scope | Purpose | +|-------|---------| +| `channels:history` | Read messages in public channels | +| `channels:read` | List and find public channels | +| `chat:write` | Post messages | +| `chat:write.customize` | Post messages with custom username and avatar (required for username spoofing) | +| `files:read` | Access shared files | +| `files:write` | Upload files | +| `groups:history` | Read messages in private channels | +| `groups:read` | List and find private channels | +| `reactions:read` | Read emoji reactions | +| `users:read` | Look up user info for display names and avatars | + +5. Under *Event Subscriptions > Subscribe to bot events*, add: + - `message.channels` — messages in public channels + - `message.groups` — messages in private channels + - `member_joined_channel` — channel membership changes + +6. Install the app to your workspace. The **Bot User OAuth Token** is your `Token`. + +> **Note:** Without the `chat:write.customize` scope, all bridged messages will appear under the bot's own name instead of showing the original sender's username and avatar. + +#### Discord bot setup + +1. Create a bot at +2. Under *Bot > Privileged Gateway Intents*, enable: + - **Message Content Intent** — required to read message text, embeds, and attachments + - **Server Members Intent** — required for member/nick resolution +3. Under *OAuth2 > URL Generator*, select the `bot` scope with these **Bot Permissions**: + - **Read Messages/View Channels** + - **Send Messages** + - **Manage Webhooks** — required when using `AutoWebhooks=true` (recommended for username/avatar spoofing) + - **Manage Roles** — optional, allows role mentions to display with names instead of IDs +4. Use the generated URL to invite the bot to your server + ## Running See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. diff --git a/bridge/config/config.go b/bridge/config/config.go index 75792ed0c3..b484970a37 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -163,6 +163,7 @@ type Protocol struct { TeamID string // msteams TenantID string // msteams Token string // gitter, slack, discord, api, matrix + AppToken string // slack Topic string // zulip URL string // mattermost, slack // DEPRECATED UseAPI bool // mattermost, slack diff --git a/bridge/discord/discord.go b/bridge/discord/discord.go index 3f99da0cee..42b028b09d 100644 --- a/bridge/discord/discord.go +++ b/bridge/discord/discord.go @@ -81,10 +81,9 @@ func (b *Bdiscord) Connect() error { return err } b.Log.Info("Connection succeeded") - // Add privileged intent for guild member tracking. This is needed to track nicks - // for display names and @mention translation - b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged | - discordgo.IntentsGuildMembers) + b.c.Identify.Intents = discordgo.IntentsAllWithoutPrivileged | + discordgo.IntentsGuildMembers | + discordgo.IntentMessageContent err = b.c.Open() if err != nil { diff --git a/bridge/discord/helpers.go b/bridge/discord/helpers.go index 4284bf5e64..ef320605a4 100644 --- a/bridge/discord/helpers.go +++ b/bridge/discord/helpers.go @@ -38,11 +38,10 @@ func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string { defer b.membersMutex.RUnlock() if member, ok := b.userMemberMap[user.ID]; ok { - if member.Nick != "" { - // Only return if nick is set. - return member.Nick + // Use Discord's display name priority: server nick > global display name > username + if name := member.DisplayName(); name != "" { + return name } - // Otherwise return username. return user.Username } @@ -57,9 +56,9 @@ func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string { } b.userMemberMap[user.ID] = member b.nickMemberMap[member.User.Username] = member - if member.Nick != "" { - b.nickMemberMap[member.Nick] = member - return member.Nick + if name := member.DisplayName(); name != "" { + b.nickMemberMap[name] = member + return name } return user.Username } diff --git a/bridge/slack/handlers.go b/bridge/slack/handlers.go index 242444276e..e5e0c833c7 100644 --- a/bridge/slack/handlers.go +++ b/bridge/slack/handlers.go @@ -9,6 +9,8 @@ import ( "github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/helper" "github.com/slack-go/slack" + "github.com/slack-go/slack/slackevents" + "github.com/slack-go/slack/socketmode" ) // ErrEventIgnored is for events that should be ignored @@ -48,65 +50,76 @@ func (b *Bslack) handleSlack() { } func (b *Bslack) handleSlackClient(messages chan *config.Message) { - for msg := range b.rtm.IncomingEvents { - if msg.Type != sUserTyping && msg.Type != sHello && msg.Type != sLatencyReport { - b.Log.Debugf("== Receiving event %#v", msg.Data) - } - switch ev := msg.Data.(type) { - case *slack.UserTypingEvent: - if !b.GetBool("ShowUserTyping") { - continue - } - rmsg, err := b.handleTypingEvent(ev) - if err == ErrEventIgnored { - continue - } else if err != nil { - b.Log.Errorf("%#v", err) - continue + for msg := range b.smc.Events { + switch msg.Type { + case socketmode.EventTypeConnected: + if authTest, authErr := b.smc.AuthTest(); authErr == nil { + if botInfo, infoErr := b.smc.GetBotInfo(slack.GetBotInfoParameters{Bot: authTest.BotID}); infoErr == nil { + b.si = botInfo + + b.channels.populateChannels(true) + b.users.populateUsers(true) + } else { + b.Log.Fatalf("Unable to identify bot user") + } + } else { + b.Log.Fatalf("Unable to identify bot user") } + case socketmode.EventTypeConnectionError: + ev, _ := msg.Data.(slack.ConnectionErrorEvent) + + b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) + case socketmode.EventTypeErrorWriteFailed: + ev, _ := msg.Data.(socketmode.ErrorWriteFailed) + + b.Log.Debugf("%#v", ev.Cause.Error()) + case socketmode.EventTypeInvalidAuth: + ev, _ := msg.Data.(slack.InvalidAuthEvent) + + b.Log.Fatalf("Invalid Token %#v", ev) + case socketmode.EventTypeDisconnect: + b.Log.Info("Socket Mode connection disconnected, will reconnect automatically") + case socketmode.EventTypeHello, socketmode.EventTypeConnecting: + continue + + case socketmode.EventTypeEventsAPI: + b.smc.Ack(*msg.Request) + + eventsAPIEvent, ok := msg.Data.(slackevents.EventsAPIEvent) + + if !ok { + b.Log.Debugf("Ignored %+v", eventsAPIEvent) - messages <- rmsg - case *slack.MessageEvent: - if b.skipMessageEvent(ev) { - b.Log.Debugf("Skipped message: %#v", ev) - continue - } - rmsg, err := b.handleMessageEvent(ev) - if err != nil { - b.Log.Errorf("%#v", err) continue } - messages <- rmsg - case *slack.FileDeletedEvent: - rmsg, err := b.handleFileDeletedEvent(ev) - if err != nil { - b.Log.Printf("%#v", err) - continue + + switch innerEventData := eventsAPIEvent.InnerEvent.Data.(type) { + case *slackevents.MessageEvent: + if b.skipMessageEvent(innerEventData) { + b.Log.Debugf("Skipped message: %#v", innerEventData) + continue + } + rmsg, err := b.handleMessageEvent(innerEventData) + if err != nil { + b.Log.Errorf("%#v", err) + continue + } + messages <- rmsg + case *slackevents.MemberJoinedChannelEvent: + if innerEventData.User == b.si.UserID { + channel, err := b.smc.GetConversationInfo(&slack.GetConversationInfoInput{ChannelID: innerEventData.Channel}) + + if err != nil { + b.Log.Errorf("Unable to get conversation info for channel %s", innerEventData.Channel) + } + + b.channels.registerChannel(*channel) + } else { + b.users.populateUser(innerEventData.User) + } } - messages <- rmsg - case *slack.OutgoingErrorEvent: - b.Log.Debugf("%#v", ev.Error()) - case *slack.ChannelJoinedEvent: - // When we join a channel we update the full list of users as - // well as the information for the channel that we joined as this - // should now tell that we are a member of it. - b.channels.registerChannel(ev.Channel) - case *slack.ConnectedEvent: - b.si = ev.Info - b.channels.populateChannels(true) - b.users.populateUsers(true) - case *slack.InvalidAuthEvent: - b.Log.Fatalf("Invalid Token %#v", ev) - case *slack.ConnectionErrorEvent: - b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) - case *slack.MemberJoinedChannelEvent: - b.users.populateUser(ev.User) - case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent: - continue - case *slack.UserChangeEvent: - b.users.invalidateUser(ev.User.ID) default: - b.Log.Debugf("Unhandled incoming event: %T", ev) + b.Log.Debugf("== Receiving event %#v", msg.Data) } } } @@ -127,7 +140,7 @@ func (b *Bslack) handleMatterHook(messages chan *config.Message) { } // skipMessageEvent skips event that need to be skipped :-) -func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { +func (b *Bslack) skipMessageEvent(ev *slackevents.MessageEvent) bool { switch ev.SubType { case sChannelLeave, sChannelJoin: return b.GetBool(noSendJoinConfig) @@ -135,39 +148,83 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { return true case sChannelTopic, sChannelPurpose: // Skip the event if our bot/user account changed the topic/purpose - if ev.User == b.si.User.ID { + if ev.BotID == b.si.ID { return true } } - // Check for our callback ID + // Check for our callback ID in blocks. + // For Socket Mode / Events API, blocks are at the top level (ev.Blocks), + // not inside attachments. Check both locations for compatibility. + // Iterate ALL blocks since Slack can inject additional blocks (e.g. rich_text) + // before our SectionBlock. hasOurCallbackID := false - if len(ev.Blocks.BlockSet) == 1 { - block, ok := ev.Blocks.BlockSet[0].(*slack.SectionBlock) - hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid + callbackBlockID := "matterbridge_" + b.uuid + + // Check top-level blocks (Socket Mode / Events API) + for _, blk := range ev.Blocks.BlockSet { + if section, ok := blk.(*slack.SectionBlock); ok && section.BlockID == callbackBlockID { + hasOurCallbackID = true + break + } + } + // Check blocks inside attachments (legacy / RTM) + if !hasOurCallbackID { + for _, att := range ev.Attachments { + for _, blk := range att.Blocks.BlockSet { + if section, ok := blk.(*slack.SectionBlock); ok && section.BlockID == callbackBlockID { + hasOurCallbackID = true + break + } + } + if hasOurCallbackID { + break + } + } } - if ev.SubMessage != nil { - // It seems ev.SubMessage.Edited == nil when slack unfurls. + if ev.Message != nil { + // It seems ev.Message.Edited == nil when slack unfurls. // Do not forward these messages. See Github issue #266. - if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp && - ev.SubMessage.Edited == nil { + if ev.Message.ThreadTimeStamp != ev.Message.TimeStamp && + ev.Message.Edited == nil { return true } - // see hidden subtypes at https://api.slack.com/events/message - // these messages are sent when we add a message to a thread #709 - if ev.SubType == "message_replied" && ev.Hidden { - return true + + // Check top-level blocks on inner message (message_changed events via Socket Mode) + if !hasOurCallbackID { + for _, blk := range ev.Message.Blocks.BlockSet { + if section, ok := blk.(*slack.SectionBlock); ok && section.BlockID == callbackBlockID { + hasOurCallbackID = true + break + } + } } - if len(ev.SubMessage.Blocks.BlockSet) == 1 { - block, ok := ev.SubMessage.Blocks.BlockSet[0].(*slack.SectionBlock) - hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid + // Check blocks inside attachments on inner message (legacy / RTM) + if !hasOurCallbackID { + for _, att := range ev.Message.Attachments { + for _, blk := range att.Blocks.BlockSet { + if section, ok := blk.(*slack.SectionBlock); ok && section.BlockID == callbackBlockID { + hasOurCallbackID = true + break + } + } + if hasOurCallbackID { + break + } + } } } // Skip any messages that we made ourselves or from 'slackbot' (see #527). + // For message_changed events, the bot ID is on ev.Message.BotID, not ev.BotID. + // Also check ev.User against the bot's UserID, as the BotID field may not always + // match b.si.ID depending on the Slack app configuration. if ev.Username == sSlackBotUser || - (b.rtm != nil && ev.Username == b.si.User.Name) || hasOurCallbackID { + (b.smc != nil && ev.BotID == b.si.ID) || + (b.smc != nil && ev.User == b.si.UserID) || + (b.smc != nil && ev.Message != nil && ev.Message.BotID == b.si.ID) || + hasOurCallbackID { return true } @@ -177,7 +234,7 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { return false } -func (b *Bslack) filesCached(files []slack.File) bool { +func (b *Bslack) filesCached(files []slackevents.File) bool { for i := range files { if !b.fileCached(&files[i]) { return false @@ -189,20 +246,20 @@ func (b *Bslack) filesCached(files []slack.File) bool { // handleMessageEvent handles the message events. Together with any called sub-methods, // this method implements the following event processing pipeline: // -// 1. Check if the message should be ignored. -// NOTE: This is not actually part of the method below but is done just before it -// is called via the 'skipMessageEvent()' method. -// 2. Populate the Matterbridge message that will be sent to the router based on the -// received event and logic that is common to all events that are not skipped. -// 3. Detect and handle any message that is "status" related (think join channel, etc.). -// This might result in an early exit from the pipeline and passing of the -// pre-populated message to the Matterbridge router. -// 4. Handle the specific case of messages that edit existing messages depending on -// configuration. -// 5. Handle any attachments of the received event. -// 6. Check that the Matterbridge message that we end up with after at the end of the -// pipeline is valid before sending it to the Matterbridge router. -func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, error) { +// 1. Check if the message should be ignored. +// NOTE: This is not actually part of the method below but is done just before it +// is called via the 'skipMessageEvent()' method. +// 2. Populate the Matterbridge message that will be sent to the router based on the +// received event and logic that is common to all events that are not skipped. +// 3. Detect and handle any message that is "status" related (think join channel, etc.). +// This might result in an early exit from the pipeline and passing of the +// pre-populated message to the Matterbridge router. +// 4. Handle the specific case of messages that edit existing messages depending on +// configuration. +// 5. Handle any attachments of the received event. +// 6. Check that the Matterbridge message that we end up with after at the end of the +// pipeline is valid before sending it to the Matterbridge router. +func (b *Bslack) handleMessageEvent(ev *slackevents.MessageEvent) (*config.Message, error) { rmsg, err := b.populateReceivedMessage(ev) if err != nil { return nil, err @@ -222,35 +279,15 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er // This is probably a webhook we couldn't resolve. return nil, fmt.Errorf("message handling resulted in an empty bot message (probably an incoming webhook we couldn't resolve): %#v", ev) } - if ev.SubMessage != nil { - return nil, fmt.Errorf("message handling resulted in an empty message: %#v with submessage %#v", ev, ev.SubMessage) + if ev.Message != nil { + return nil, fmt.Errorf("message handling resulted in an empty message: %#v with submessage %#v", ev, ev.Message) } return nil, fmt.Errorf("message handling resulted in an empty message: %#v", ev) } return rmsg, nil } -func (b *Bslack) handleFileDeletedEvent(ev *slack.FileDeletedEvent) (*config.Message, error) { - if rawChannel, ok := b.cache.Get(cfileDownloadChannel + ev.FileID); ok { - channel, err := b.channels.getChannelByID(rawChannel.(string)) - if err != nil { - return nil, err - } - - return &config.Message{ - Event: config.EventFileDelete, - Text: config.EventFileDelete, - Channel: channel.Name, - Account: b.Account, - ID: ev.FileID, - Protocol: b.Protocol, - }, nil - } - - return nil, fmt.Errorf("channel ID for file ID %s not found", ev.FileID) -} - -func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool { +func (b *Bslack) handleStatusEvent(ev *slackevents.MessageEvent, rmsg *config.Message) bool { switch ev.SubType { case sChannelJoined, sMemberJoined: // There's no further processing needed on channel events @@ -263,16 +300,16 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) b.channels.populateChannels(false) rmsg.Event = config.EventTopicChange case sMessageChanged: - rmsg.Text = ev.SubMessage.Text + rmsg.Text = ev.Message.Text // handle deleted thread starting messages - if ev.SubMessage.Text == "This message was deleted." { + if ev.Message.Text == "This message was deleted." { rmsg.Event = config.EventMsgDelete return true } case sMessageDeleted: rmsg.Text = config.EventMsgDelete rmsg.Event = config.EventMsgDelete - rmsg.ID = ev.DeletedTimestamp + rmsg.ID = ev.PreviousMessage.TimeStamp // If a message is being deleted we do not need to process // the event any further so we return 'true'. return true @@ -289,7 +326,7 @@ func getMessageTitle(attach *slack.Attachment) string { return attach.Title } -func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) { +func (b *Bslack) handleAttachments(ev *slackevents.MessageEvent, rmsg *config.Message) { // File comments are set by the system (because there is no username given). if ev.SubType == sFileComment { rmsg.Username = sSystemUser @@ -327,23 +364,8 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) } } -func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) { - if ev.User == b.si.User.ID { - return nil, ErrEventIgnored - } - channelInfo, err := b.channels.getChannelByID(ev.Channel) - if err != nil { - return nil, err - } - return &config.Message{ - Channel: channelInfo.Name, - Account: b.Account, - Event: config.EventUserTyping, - }, nil -} - // handleDownloadFile handles file download -func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retry bool) error { +func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slackevents.File, retry bool) error { if b.fileCached(file) { return nil } @@ -405,7 +427,7 @@ func (b *Bslack) handleGetChannelMembers(rmsg *config.Message) bool { // identically named file but with different content will be uploaded correctly // (the assumption is that such name collisions will not occur within the given // timeframes). -func (b *Bslack) fileCached(file *slack.File) bool { +func (b *Bslack) fileCached(file *slackevents.File) bool { if ts, ok := b.cache.Get("file" + file.ID); ok && time.Since(ts.(time.Time)) < time.Minute { return true } else if ts, ok = b.cache.Get("filename" + file.Name); ok && time.Since(ts.(time.Time)) < 10*time.Second { diff --git a/bridge/slack/helpers.go b/bridge/slack/helpers.go index 309b3af84e..4c1b897949 100644 --- a/bridge/slack/helpers.go +++ b/bridge/slack/helpers.go @@ -9,11 +9,12 @@ import ( "github.com/42wim/matterbridge/bridge/config" "github.com/sirupsen/logrus" "github.com/slack-go/slack" + "github.com/slack-go/slack/slackevents" ) // populateReceivedMessage shapes the initial Matterbridge message that we will forward to the // router before we apply message-dependent modifications. -func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Message, error) { +func (b *Bslack) populateReceivedMessage(ev *slackevents.MessageEvent) (*config.Message, error) { // Use our own func because rtm.GetChannelInfo doesn't work for private channels. channel, err := b.channels.getChannelByID(ev.Channel) if err != nil { @@ -24,9 +25,9 @@ func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Messag Text: ev.Text, Channel: channel.Name, Account: b.Account, - ID: ev.Timestamp, + ID: ev.TimeStamp, Extra: make(map[string][]interface{}), - ParentID: ev.ThreadTimestamp, + ParentID: ev.ThreadTimeStamp, Protocol: b.Protocol, } if b.useChannelID { @@ -34,19 +35,19 @@ func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Messag } // Handle 'edit' messages. - if ev.SubMessage != nil && !b.GetBool(editDisableConfig) { - rmsg.ID = ev.SubMessage.Timestamp - if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp { - b.Log.Debugf("SubMessage %#v", ev.SubMessage) - rmsg.Text = ev.SubMessage.Text + b.GetString(editSuffixConfig) + if ev.Message != nil && !b.GetBool(editDisableConfig) { + rmsg.ID = ev.Message.TimeStamp + if ev.Message.ThreadTimeStamp != ev.Message.TimeStamp { + b.Log.Debugf("SubMessage %#v", ev.Message) + rmsg.Text = ev.Message.Text + b.GetString(editSuffixConfig) } } // For edits, only submessage has thread ts. // Ensures edits to threaded messages maintain their prefix hint on the // unthreaded end. - if ev.SubMessage != nil { - rmsg.ParentID = ev.SubMessage.ThreadTimestamp + if ev.Message != nil { + rmsg.ParentID = ev.Message.ThreadTimeStamp } if err = b.populateMessageWithUserInfo(ev, rmsg); err != nil { @@ -55,7 +56,7 @@ func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Messag return rmsg, err } -func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *config.Message) error { +func (b *Bslack) populateMessageWithUserInfo(ev *slackevents.MessageEvent, rmsg *config.Message) error { if ev.SubType == sMessageDeleted || ev.SubType == sFileComment { return nil } @@ -71,8 +72,8 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi switch { case ev.User != "": userID = ev.User - case ev.SubMessage != nil && ev.SubMessage.User != "": - userID = ev.SubMessage.User + case ev.Message != nil && ev.Message.User != "": + userID = ev.Message.User default: return nil } @@ -84,6 +85,9 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi rmsg.UserID = user.ID rmsg.Username = user.Name + if user.Profile.RealName != "" { + rmsg.Username = user.Profile.RealName + } if user.Profile.DisplayName != "" { rmsg.Username = user.Profile.DisplayName } @@ -93,7 +97,7 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi return nil } -func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config.Message) error { +func (b *Bslack) populateMessageWithBotInfo(ev *slackevents.MessageEvent, rmsg *config.Message) error { if ev.BotID == "" || b.GetString(outgoingWebhookConfig) != "" { return nil } @@ -101,9 +105,7 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config var err error var bot *slack.Bot for { - bot, err = b.rtm.GetBotInfo(slack.GetBotInfoParameters{ - Bot: ev.BotID, - }) + bot, err = b.smc.GetBotInfo(slack.GetBotInfoParameters{Bot: ev.BotID}) if err == nil { break } diff --git a/bridge/slack/slack.go b/bridge/slack/slack.go index c39c60826d..28fdd4372c 100644 --- a/bridge/slack/slack.go +++ b/bridge/slack/slack.go @@ -15,6 +15,7 @@ import ( lru "github.com/hashicorp/golang-lru" "github.com/rs/xid" "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" ) type Bslack struct { @@ -23,8 +24,11 @@ type Bslack struct { mh *matterhook.Client sc *slack.Client + si *slack.Bot + smc *socketmode.Client + + // rtm is only used by the legacy (non-bot-token) bridge. rtm *slack.RTM - si *slack.Info cache *lru.Cache uuid string @@ -36,27 +40,25 @@ type Bslack struct { } const ( - sHello = "hello" - sChannelJoin = "channel_join" - sChannelLeave = "channel_leave" - sChannelJoined = "channel_joined" - sMemberJoined = "member_joined_channel" - sMessageChanged = "message_changed" - sMessageDeleted = "message_deleted" - sSlackAttachment = "slack_attachment" - sPinnedItem = "pinned_item" - sUnpinnedItem = "unpinned_item" - sChannelTopic = "channel_topic" - sChannelPurpose = "channel_purpose" - sFileComment = "file_comment" - sMeMessage = "me_message" - sUserTyping = "user_typing" - sLatencyReport = "latency_report" + sChannelJoin = "channel_join" + sChannelLeave = "channel_leave" + sChannelJoined = "channel_joined" + sMemberJoined = "member_joined_channel" + sMessageChanged = "message_changed" + sMessageDeleted = "message_deleted" + sSlackAttachment = "slack_attachment" + sPinnedItem = "pinned_item" + sUnpinnedItem = "unpinned_item" + sChannelTopic = "channel_topic" + sChannelPurpose = "channel_purpose" + sFileComment = "file_comment" + sMeMessage = "me_message" sSystemUser = "system" sSlackBotUser = "slackbot" cfileDownloadChannel = "file_download_channel" tokenConfig = "Token" + appTokenConfig = "AppToken" incomingWebhookConfig = "WebhookBindAddress" outgoingWebhookConfig = "WebhookURL" skipTLSConfig = "SkipTLSVerify" @@ -71,12 +73,21 @@ const ( func New(cfg *bridge.Config) bridge.Bridger { // Print a deprecation warning for legacy non-bot tokens (#527). token := cfg.GetString(tokenConfig) + appToken := cfg.GetString(appTokenConfig) if token != "" && !strings.HasPrefix(token, "xoxb") { cfg.Log.Warn("Non-bot token detected. It is STRONGLY recommended to use a proper bot-token instead.") cfg.Log.Warn("Legacy tokens may be deprecated by Slack at short notice. See the Matterbridge GitHub wiki for a migration guide.") cfg.Log.Warn("See https://github.com/42wim/matterbridge/wiki/Slack-bot-setup") return NewLegacy(cfg) } + + if token != "" { + if appToken == "" || !strings.HasPrefix(appToken, "xapp-") { + cfg.Log.Fatalf("Account %s: %s is missing or invalid (must start with xapp-), got %q", + cfg.Account, appTokenConfig, appToken) + } + } + return newBridge(cfg) } @@ -105,18 +116,22 @@ func (b *Bslack) Connect() error { return errors.New("no connection method found: WebhookBindAddress, WebhookURL or Token need to be configured") } - // If we have a token we use the Slack websocket-based RTM for both sending and receiving. - if token := b.GetString(tokenConfig); token != "" { + token := b.GetString(tokenConfig) + appToken := b.GetString(appTokenConfig) + + // If we have a token we use the Slack websocket-based Socket Mode for both sending and receiving. + if token != "" && appToken != "" { b.Log.Info("Connecting using token") - b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug"))) + b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug")), slack.OptionAppLevelToken(appToken)) + b.smc = socketmode.New(b.sc, socketmode.OptionDebug(b.GetBool("Debug"))) b.channels = newChannelManager(b.Log, b.sc) b.users = newUserManager(b.Log, b.sc) - b.rtm = b.sc.NewRTM() - go b.rtm.ManageConnection() go b.handleSlack() + go b.smc.Run() + return nil } @@ -142,7 +157,7 @@ func (b *Bslack) Connect() error { } func (b *Bslack) Disconnect() error { - return b.rtm.Disconnect() + return nil } // JoinChannel only acts as a verification method that checks whether Matterbridge's @@ -208,7 +223,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) { if b.GetString(outgoingWebhookConfig) != "" && b.GetString(tokenConfig) == "" { return "", b.sendWebhook(msg) } - return b.sendRTM(msg) + return b.sendSocket(msg) } // sendWebhook uses the configured WebhookURL to send the message @@ -275,7 +290,7 @@ func (b *Bslack) sendWebhook(msg config.Message) error { return nil } -func (b *Bslack) sendRTM(msg config.Message) (string, error) { +func (b *Bslack) sendSocket(msg config.Message) (string, error) { // Handle channelmember messages. if handled := b.handleGetChannelMembers(&msg); handled { return "", nil @@ -285,13 +300,6 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) { if err != nil { return "", fmt.Errorf("could not send message: %v", err) } - if msg.Event == config.EventUserTyping { - if b.GetBool("ShowUserTyping") { - b.rtm.SendMessage(b.rtm.NewTypingMessage(channelInfo.ID)) - } - return "", nil - } - var handled bool // Handle topic/purpose updates. @@ -345,9 +353,9 @@ func (b *Bslack) updateTopicOrPurpose(msg *config.Message, channelInfo *slack.Ch incomingChangeType, text := b.extractTopicOrPurpose(msg.Text) switch incomingChangeType { case "topic": - updateFunc = b.rtm.SetTopicOfConversation + updateFunc = b.smc.SetTopicOfConversation case "purpose": - updateFunc = b.rtm.SetPurposeOfConversation + updateFunc = b.smc.SetPurposeOfConversation default: b.Log.Errorf("Unhandled type received from extractTopicOrPurpose: %s", incomingChangeType) return nil @@ -393,7 +401,7 @@ func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel) } for { - _, _, err := b.rtm.DeleteMessage(channelInfo.ID, msg.ID) + _, _, err := b.smc.DeleteMessage(channelInfo.ID, msg.ID) if err == nil { return true, nil } @@ -411,7 +419,7 @@ func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (b } messageOptions := b.prepareMessageOptions(msg) for { - _, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...) + _, _, _, err := b.smc.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...) if err == nil { return true, nil } @@ -430,7 +438,7 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s } messageOptions := b.prepareMessageOptions(msg) for { - _, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...) + _, id, err := b.smc.PostMessage(channelInfo.ID, messageOptions...) if err == nil { return id, nil } diff --git a/bridge/slack/users_channels.go b/bridge/slack/users_channels.go index 85b944bd50..2bb6d4c695 100644 --- a/bridge/slack/users_channels.go +++ b/bridge/slack/users_channels.go @@ -56,6 +56,9 @@ func (b *users) getUsername(id string) string { if user.Profile.DisplayName != "" { return user.Profile.DisplayName } + if user.Profile.RealName != "" { + return user.Profile.RealName + } return user.Name } b.log.Warnf("Could not find user with ID '%s'", id) diff --git a/matterbridge.toml.sample b/matterbridge.toml.sample index 0665a5992f..1e054b9df9 100644 --- a/matterbridge.toml.sample +++ b/matterbridge.toml.sample @@ -639,11 +639,14 @@ PreserveThreading=false #REQUIRED [slack.hobby] #Token to connect with the Slack API -#You'll have to use a test/api-token using a dedicated user and not a bot token. -#See https://github.com/42wim/matterbridge/issues/75 for more info. -#Use https://api.slack.com/custom-integrations/legacy-tokens -#REQUIRED (when not using webhooks) -Token="yourslacktoken" +#Create a bot token on https://api.slack.com/apps - it must start with xoxb- +#REQUIRED +Token="xoxb-yourslacktoken" + +#AppToken for Socket Mode (required when using Token) +#Create an app-level token on https://api.slack.com/apps - it must start with xapp- +#REQUIRED (when using Token) +AppToken="xapp-yourapptoken" #Extra slack specific debug info, warning this generates a lot of output. #OPTIONAL (default false) @@ -802,6 +805,8 @@ UseFullName=false # Token (REQUIRED) is the token to connect with Discord API # You can get your token by following the instructions on # https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token +# Ensure your bot has the MESSAGE_CONTENT privileged intent enabled in the Developer Portal +# to read message content, embeds, and attachments from messages the bot didn't send. # If you want roles/groups mentions to be shown with names instead of ID, you'll need to give your bot the "Manage Roles" permission. Token="Yourtokenhere" diff --git a/release_1.27/matterbridge-1.27-darwin-amd64 b/release_1.27/matterbridge-1.27-darwin-amd64 new file mode 100755 index 0000000000..05958ead51 Binary files /dev/null and b/release_1.27/matterbridge-1.27-darwin-amd64 differ diff --git a/release_1.27/matterbridge-1.27-darwin-arm64 b/release_1.27/matterbridge-1.27-darwin-arm64 new file mode 100755 index 0000000000..d644baf26a Binary files /dev/null and b/release_1.27/matterbridge-1.27-darwin-arm64 differ diff --git a/release_1.27/matterbridge-1.27-linux-amd64 b/release_1.27/matterbridge-1.27-linux-amd64 new file mode 100755 index 0000000000..2294ebccd5 Binary files /dev/null and b/release_1.27/matterbridge-1.27-linux-amd64 differ diff --git a/release_1.27/matterbridge-1.27-linux-arm64 b/release_1.27/matterbridge-1.27-linux-arm64 new file mode 100755 index 0000000000..ee5f35e160 Binary files /dev/null and b/release_1.27/matterbridge-1.27-linux-arm64 differ diff --git a/release_1.27/matterbridge-1.27-linux-armv7 b/release_1.27/matterbridge-1.27-linux-armv7 new file mode 100755 index 0000000000..698a91e713 Binary files /dev/null and b/release_1.27/matterbridge-1.27-linux-armv7 differ diff --git a/release_1.27/matterbridge-1.27-windows-amd64.exe b/release_1.27/matterbridge-1.27-windows-amd64.exe new file mode 100755 index 0000000000..52cffbaf3a Binary files /dev/null and b/release_1.27/matterbridge-1.27-windows-amd64.exe differ diff --git a/release_1.27/matterbridge-1.27-windows-arm64.exe b/release_1.27/matterbridge-1.27-windows-arm64.exe new file mode 100755 index 0000000000..1dd7b53a05 Binary files /dev/null and b/release_1.27/matterbridge-1.27-windows-arm64.exe differ diff --git a/vendor/github.com/slack-go/slack/slackevents/action_events.go b/vendor/github.com/slack-go/slack/slackevents/action_events.go new file mode 100644 index 0000000000..c6016f1079 --- /dev/null +++ b/vendor/github.com/slack-go/slack/slackevents/action_events.go @@ -0,0 +1,36 @@ +package slackevents + +import ( + "encoding/json" + + "github.com/slack-go/slack" +) + +type MessageActionResponse struct { + ResponseType string `json:"response_type"` + ReplaceOriginal bool `json:"replace_original"` + Text string `json:"text"` +} + +type MessageActionEntity struct { + ID string `json:"id"` + Domain string `json:"domain"` + Name string `json:"name"` +} + +type MessageAction struct { + Type string `json:"type"` + Actions []slack.AttachmentAction `json:"actions"` + CallbackID string `json:"callback_id"` + Team MessageActionEntity `json:"team"` + Channel MessageActionEntity `json:"channel"` + User MessageActionEntity `json:"user"` + ActionTimestamp json.Number `json:"action_ts"` + MessageTimestamp json.Number `json:"message_ts"` + AttachmentID json.Number `json:"attachment_id"` + Token string `json:"token"` + Message slack.Message `json:"message"` + OriginalMessage slack.Message `json:"original_message"` + ResponseURL string `json:"response_url"` + TriggerID string `json:"trigger_id"` +} diff --git a/vendor/github.com/slack-go/slack/slackevents/inner_events.go b/vendor/github.com/slack-go/slack/slackevents/inner_events.go new file mode 100644 index 0000000000..8f8effaae6 --- /dev/null +++ b/vendor/github.com/slack-go/slack/slackevents/inner_events.go @@ -0,0 +1,1353 @@ +// inner_events.go provides EventsAPI particular inner events + +package slackevents + +import ( + "github.com/slack-go/slack" +) + +// EventsAPIInnerEvent the inner event of a EventsAPI event_callback Event. +type EventsAPIInnerEvent struct { + Type string `json:"type"` + Data interface{} +} + +// AppMentionEvent is an (inner) EventsAPI subscribable event. +type AppMentionEvent struct { + Type string `json:"type"` + User string `json:"user"` + Text string `json:"text"` + TimeStamp string `json:"ts"` + ThreadTimeStamp string `json:"thread_ts"` + Channel string `json:"channel"` + EventTimeStamp string `json:"event_ts"` + + // When Message comes from a channel that is shared between workspaces + UserTeam string `json:"user_team,omitempty"` + SourceTeam string `json:"source_team,omitempty"` + + // BotID is filled out when a bot triggers the app_mention event + BotID string `json:"bot_id,omitempty"` + + // When the app is mentioned in the edited message + Edited *Edited `json:"edited,omitempty"` +} + +// AppHomeOpenedEvent Your Slack app home was opened. +type AppHomeOpenedEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` + EventTimeStamp string `json:"event_ts"` + Tab string `json:"tab"` + View slack.View `json:"view"` +} + +// AppUninstalledEvent Your Slack app was uninstalled. +type AppUninstalledEvent struct { + Type string `json:"type"` +} + +// ChannelCreatedEvent represents the Channel created event +type ChannelCreatedEvent struct { + Type string `json:"type"` + Channel ChannelCreatedInfo `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelDeletedEvent represents the Channel deleted event +type ChannelDeletedEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelArchiveEvent represents the Channel archive event +type ChannelArchiveEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + User string `json:"user"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelUnarchiveEvent represents the Channel unarchive event +type ChannelUnarchiveEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + User string `json:"user"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelLeftEvent represents the Channel left event +type ChannelLeftEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelRenameEvent represents the Channel rename event +type ChannelRenameEvent struct { + Type string `json:"type"` + Channel ChannelRenameInfo `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelIDChangedEvent represents the Channel identifier changed event +type ChannelIDChangedEvent struct { + Type string `json:"type"` + OldChannelID string `json:"old_channel_id"` + NewChannelID string `json:"new_channel_id"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelCreatedInfo represents the information associated with the Channel created event +type ChannelCreatedInfo struct { + ID string `json:"id"` + IsChannel bool `json:"is_channel"` + Name string `json:"name"` + Created int `json:"created"` + Creator string `json:"creator"` +} + +// ChannelRenameInfo represents the information associated with the Channel rename event +type ChannelRenameInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Created int `json:"created"` +} + +// GroupDeletedEvent represents the Group deleted event +type GroupDeletedEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// GroupArchiveEvent represents the Group archive event +type GroupArchiveEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// GroupUnarchiveEvent represents the Group unarchive event +type GroupUnarchiveEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// GroupLeftEvent represents the Group left event +type GroupLeftEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// GroupRenameEvent represents the Group rename event +type GroupRenameEvent struct { + Type string `json:"type"` + Channel GroupRenameInfo `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// GroupRenameInfo represents the information associated with the Group rename event +type GroupRenameInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Created int `json:"created"` +} + +// FileChangeEvent represents the information associated with the File change +// event. +type FileChangeEvent struct { + Type string `json:"type"` + FileID string `json:"file_id"` + File FileEventFile `json:"file"` +} + +// FileDeletedEvent represents the information associated with the File deleted +// event. +type FileDeletedEvent struct { + Type string `json:"type"` + FileID string `json:"file_id"` + EventTimestamp string `json:"event_ts"` +} + +// FileSharedEvent represents the information associated with the File shared +// event. +type FileSharedEvent struct { + Type string `json:"type"` + ChannelID string `json:"channel_id"` + FileID string `json:"file_id"` + UserID string `json:"user_id"` + File FileEventFile `json:"file"` + EventTimestamp string `json:"event_ts"` +} + +// FileUnsharedEvent represents the information associated with the File +// unshared event. +type FileUnsharedEvent struct { + Type string `json:"type"` + FileID string `json:"file_id"` + File FileEventFile `json:"file"` +} + +// FileEventFile represents information on the specific file being shared in a +// file-related Slack event. +type FileEventFile struct { + ID string `json:"id"` +} + +// GridMigrationFinishedEvent An enterprise grid migration has finished on this workspace. +type GridMigrationFinishedEvent struct { + Type string `json:"type"` + EnterpriseID string `json:"enterprise_id"` +} + +// GridMigrationStartedEvent An enterprise grid migration has started on this workspace. +type GridMigrationStartedEvent struct { + Type string `json:"type"` + EnterpriseID string `json:"enterprise_id"` +} + +// LinkSharedEvent A message was posted containing one or more links relevant to your application +type LinkSharedEvent struct { + Type string `json:"type"` + User string `json:"user"` + TimeStamp string `json:"ts"` + Channel string `json:"channel"` + // MessageTimeStamp can be both a numeric timestamp if the LinkSharedEvent corresponds to a sent + // message and (contrary to the field name) a uuid if the LinkSharedEvent is generated in the + // compose text area. + MessageTimeStamp string `json:"message_ts"` + ThreadTimeStamp string `json:"thread_ts"` + Links []SharedLinks `json:"links"` + EventTimestamp string `json:"event_ts"` +} + +type SharedLinks struct { + Domain string `json:"domain"` + URL string `json:"url"` +} + +// MessageEvent occurs when a variety of types of messages has been posted. +// Parse ChannelType to see which +// if ChannelType = "group", this is a private channel message +// if ChannelType = "channel", this message was sent to a channel +// if ChannelType = "im", this is a private message +// if ChannelType = "mim", A message was posted in a multiparty direct message channel +// TODO: Improve this so that it is not required to manually parse ChannelType +type MessageEvent struct { + // Basic Message Event - https://api.slack.com/events/message + ClientMsgID string `json:"client_msg_id"` + Type string `json:"type"` + User string `json:"user"` + Text string `json:"text"` + ThreadTimeStamp string `json:"thread_ts"` + TimeStamp string `json:"ts"` + Channel string `json:"channel"` + ChannelType string `json:"channel_type"` + EventTimeStamp string `json:"event_ts"` + + // When Message comes from a channel that is shared between workspaces + UserTeam string `json:"user_team,omitempty"` + SourceTeam string `json:"source_team,omitempty"` + + // Edited Message + Message *MessageEvent `json:"message,omitempty"` + PreviousMessage *MessageEvent `json:"previous_message,omitempty"` + Edited *Edited `json:"edited,omitempty"` + + // Deleted Message + DeletedTimeStamp string `json:"deleted_ts,omitempty"` + + // Message Subtypes + SubType string `json:"subtype,omitempty"` + + // bot_message (https://api.slack.com/events/message/bot_message) + BotID string `json:"bot_id,omitempty"` + Username string `json:"username,omitempty"` + Icons *Icon `json:"icons,omitempty"` + + Upload bool `json:"upload"` + Files []File `json:"files"` + + Blocks slack.Blocks `json:"blocks,omitempty"` + Attachments []slack.Attachment `json:"attachments,omitempty"` + + Metadata slack.SlackMetadata `json:"metadata,omitempty"` + + // Root is the message that was broadcast to the channel when the SubType is + // thread_broadcast. If this is not a thread_broadcast message event, this + // value is nil. + Root *MessageEvent `json:"root"` +} + +// MemberJoinedChannelEvent A member joined a public or private channel +type MemberJoinedChannelEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` + ChannelType string `json:"channel_type"` + Team string `json:"team"` + Inviter string `json:"inviter"` + EventTimestamp string `json:"event_ts"` +} + +// MemberLeftChannelEvent A member left a public or private channel +type MemberLeftChannelEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` + ChannelType string `json:"channel_type"` + Team string `json:"team"` + EventTimestamp string `json:"event_ts"` +} + +type pinEvent struct { + Type string `json:"type"` + User string `json:"user"` + Item Item `json:"item"` + Channel string `json:"channel_id"` + EventTimestamp string `json:"event_ts"` + HasPins bool `json:"has_pins,omitempty"` +} + +type reactionEvent struct { + Type string `json:"type"` + User string `json:"user"` + Reaction string `json:"reaction"` + ItemUser string `json:"item_user"` + Item Item `json:"item"` + EventTimestamp string `json:"event_ts"` +} + +// ReactionAddedEvent An reaction was added to a message - https://api.slack.com/events/reaction_added +type ReactionAddedEvent reactionEvent + +// ReactionRemovedEvent An reaction was removed from a message - https://api.slack.com/events/reaction_removed +type ReactionRemovedEvent reactionEvent + +// PinAddedEvent An item was pinned to a channel - https://api.slack.com/events/pin_added +type PinAddedEvent pinEvent + +// PinRemovedEvent An item was unpinned from a channel - https://api.slack.com/events/pin_removed +type PinRemovedEvent pinEvent + +type tokens struct { + Oauth []string `json:"oauth"` + Bot []string `json:"bot"` +} + +// TeamJoinEvent A new member joined a workspace - https://api.slack.com/events/team_join +type TeamJoinEvent struct { + Type string `json:"type"` + User *slack.User `json:"user"` + EventTimestamp string `json:"event_ts"` +} + +// TokensRevokedEvent APP's API tokens are revoked - https://api.slack.com/events/tokens_revoked +type TokensRevokedEvent struct { + Type string `json:"type"` + Tokens tokens `json:"tokens"` + EventTimestamp string `json:"event_ts"` +} + +// EmojiChangedEvent is the event of custom emoji has been added or changed +type EmojiChangedEvent struct { + Type string `json:"type"` + Subtype string `json:"subtype"` + EventTimeStamp string `json:"event_ts"` + + // filled out when custom emoji added + Name string `json:"name,omitempty"` + + // filled out when custom emoji removed + Names []string `json:"names,omitempty"` + + // filled out when custom emoji renamed + OldName string `json:"old_name,omitempty"` + NewName string `json:"new_name,omitempty"` + + // filled out when custom emoji added or renamed + Value string `json:"value,omitempty"` +} + +// WorkflowStepExecuteEvent is fired, if a workflow step of your app is invoked +type WorkflowStepExecuteEvent struct { + Type string `json:"type"` + CallbackID string `json:"callback_id"` + WorkflowStep EventWorkflowStep `json:"workflow_step"` + EventTimestamp string `json:"event_ts"` +} + +// MessageMetadataPostedEvent is sent, if a message with metadata is posted +type MessageMetadataPostedEvent struct { + Type string `json:"type"` + AppId string `json:"app_id"` + BotId string `json:"bot_id"` + UserId string `json:"user_id"` + TeamId string `json:"team_id"` + ChannelId string `json:"channel_id"` + Metadata *slack.SlackMetadata `json:"metadata"` + MessageTimestamp string `json:"message_ts"` + EventTimestamp string `json:"event_ts"` +} + +// MessageMetadataUpdatedEvent is sent, if a message with metadata is deleted +type MessageMetadataUpdatedEvent struct { + Type string `json:"type"` + ChannelId string `json:"channel_id"` + EventTimestamp string `json:"event_ts"` + PreviousMetadata *slack.SlackMetadata `json:"previous_metadata"` + AppId string `json:"app_id"` + BotId string `json:"bot_id"` + UserId string `json:"user_id"` + TeamId string `json:"team_id"` + MessageTimestamp string `json:"message_ts"` + Metadata *slack.SlackMetadata `json:"metadata"` +} + +// MessageMetadataDeletedEvent is sent, if a message with metadata is deleted +type MessageMetadataDeletedEvent struct { + Type string `json:"type"` + ChannelId string `json:"channel_id"` + EventTimestamp string `json:"event_ts"` + PreviousMetadata *slack.SlackMetadata `json:"previous_metadata"` + AppId string `json:"app_id"` + BotId string `json:"bot_id"` + UserId string `json:"user_id"` + TeamId string `json:"team_id"` + MessageTimestamp string `json:"message_ts"` + DeletedTimestamp string `json:"deleted_ts"` +} + +type EventWorkflowStep struct { + WorkflowStepExecuteID string `json:"workflow_step_execute_id"` + WorkflowID string `json:"workflow_id"` + WorkflowInstanceID string `json:"workflow_instance_id"` + StepID string `json:"step_id"` + Inputs *slack.WorkflowStepInputs `json:"inputs,omitempty"` + Outputs *[]slack.WorkflowStepOutput `json:"outputs,omitempty"` +} + +// JSONTime exists so that we can have a String method converting the date +type JSONTime int64 + +// Comment contains all the information relative to a comment +type Comment struct { + ID string `json:"id,omitempty"` + Created JSONTime `json:"created,omitempty"` + Timestamp JSONTime `json:"timestamp,omitempty"` + User string `json:"user,omitempty"` + Comment string `json:"comment,omitempty"` +} + +// File is a file upload +type File struct { + ID string `json:"id"` + Created int `json:"created"` + Timestamp int `json:"timestamp"` + Name string `json:"name"` + Title string `json:"title"` + Mimetype string `json:"mimetype"` + Filetype string `json:"filetype"` + PrettyType string `json:"pretty_type"` + User string `json:"user"` + Editable bool `json:"editable"` + Size int `json:"size"` + Mode string `json:"mode"` + IsExternal bool `json:"is_external"` + ExternalType string `json:"external_type"` + IsPublic bool `json:"is_public"` + PublicURLShared bool `json:"public_url_shared"` + DisplayAsBot bool `json:"display_as_bot"` + Username string `json:"username"` + URLPrivate string `json:"url_private"` + FileAccess string `json:"file_access"` + URLPrivateDownload string `json:"url_private_download"` + Thumb64 string `json:"thumb_64"` + Thumb80 string `json:"thumb_80"` + Thumb360 string `json:"thumb_360"` + Thumb360W int `json:"thumb_360_w"` + Thumb360H int `json:"thumb_360_h"` + Thumb480 string `json:"thumb_480"` + Thumb480W int `json:"thumb_480_w"` + Thumb480H int `json:"thumb_480_h"` + Thumb160 string `json:"thumb_160"` + Thumb720 string `json:"thumb_720"` + Thumb720W int `json:"thumb_720_w"` + Thumb720H int `json:"thumb_720_h"` + Thumb800 string `json:"thumb_800"` + Thumb800W int `json:"thumb_800_w"` + Thumb800H int `json:"thumb_800_h"` + Thumb960 string `json:"thumb_960"` + Thumb960W int `json:"thumb_960_w"` + Thumb960H int `json:"thumb_960_h"` + Thumb1024 string `json:"thumb_1024"` + Thumb1024W int `json:"thumb_1024_w"` + Thumb1024H int `json:"thumb_1024_h"` + ImageExifRotation int `json:"image_exif_rotation"` + OriginalW int `json:"original_w"` + OriginalH int `json:"original_h"` + Permalink string `json:"permalink"` + PermalinkPublic string `json:"permalink_public"` +} + +// Edited is included when a Message is edited +type Edited struct { + User string `json:"user"` + TimeStamp string `json:"ts"` +} + +// Icon is used for bot messages +type Icon struct { + IconURL string `json:"icon_url,omitempty"` + IconEmoji string `json:"icon_emoji,omitempty"` +} + +// Item is any type of slack message - message, file, or file comment. +type Item struct { + Type string `json:"type"` + Channel string `json:"channel,omitempty"` + Message *ItemMessage `json:"message,omitempty"` + File *File `json:"file,omitempty"` + Comment *Comment `json:"comment,omitempty"` + Timestamp string `json:"ts,omitempty"` +} + +// ItemMessage is the event message +type ItemMessage struct { + Type string `json:"type"` + User string `json:"user"` + Text string `json:"text"` + Timestamp string `json:"ts"` + PinnedTo []string `json:"pinned_to"` + ReplaceOriginal bool `json:"replace_original"` + DeleteOriginal bool `json:"delete_original"` +} + +// IsEdited checks if the MessageEvent is caused by an edit +func (e MessageEvent) IsEdited() bool { + return e.Message != nil && + e.Message.Edited != nil +} + +// TeamAccessGrantedEvent is sent if access to teams was granted for your org-wide app. +type TeamAccessGrantedEvent struct { + Type string `json:"type"` + TeamIDs []string `json:"team_ids"` +} + +// TeamAccessRevokedEvent is sent if access to teams was revoked for your org-wide app. +type TeamAccessRevokedEvent struct { + Type string `json:"type"` + TeamIDs []string `json:"team_ids"` +} + +// UserProfileChangedEvent is sent if access to teams was revoked for your org-wide app. +type UserProfileChangedEvent struct { + User *slack.User `json:"user"` + CacheTs int `json:"cache_ts"` + Type string `json:"type"` + EventTs string `json:"event_ts"` +} + +// SharedChannelInviteApprovedEvent is sent if your invitation has been approved +type SharedChannelInviteApprovedEvent struct { + Type string `json:"type"` + Invite *SharedInvite `json:"invite"` + Channel *slack.Conversation `json:"channel"` + ApprovingTeamID string `json:"approving_team_id"` + TeamsInChannel []*SlackEventTeam `json:"teams_in_channel"` + ApprovingUser *SlackEventUser `json:"approving_user"` + EventTs string `json:"event_ts"` +} + +// SharedChannelInviteAcceptedEvent is sent if external org accepts a Slack Connect channel invite +type SharedChannelInviteAcceptedEvent struct { + Type string `json:"type"` + ApprovalRequired bool `json:"approval_required"` + Invite *SharedInvite `json:"invite"` + Channel *SharedChannel `json:"channel"` + TeamsInChannel []*SlackEventTeam `json:"teams_in_channel"` + AcceptingUser *SlackEventUser `json:"accepting_user"` + EventTs string `json:"event_ts"` + RequiresSponsorship bool `json:"requires_sponsorship,omitempty"` +} + +// SharedChannelInviteDeclinedEvent is sent if external or internal org declines the Slack Connect invite +type SharedChannelInviteDeclinedEvent struct { + Type string `json:"type"` + Invite *SharedInvite `json:"invite"` + Channel *SharedChannel `json:"channel"` + DecliningTeamID string `json:"declining_team_id"` + TeamsInChannel []*SlackEventTeam `json:"teams_in_channel"` + DecliningUser *SlackEventUser `json:"declining_user"` + EventTs string `json:"event_ts"` +} + +// SharedChannelInviteReceivedEvent is sent if a bot or app is invited to a Slack Connect channel +type SharedChannelInviteReceivedEvent struct { + Type string `json:"type"` + Invite *SharedInvite `json:"invite"` + Channel *SharedChannel `json:"channel"` + EventTs string `json:"event_ts"` +} + +// SlackEventTeam is a struct for teams in ShareChannel events +type SlackEventTeam struct { + ID string `json:"id"` + Name string `json:"name"` + Icon *SlackEventIcon `json:"icon,omitempty"` + AvatarBaseURL string `json:"avatar_base_url,omitempty"` + IsVerified bool `json:"is_verified"` + Domain string `json:"domain"` + DateCreated int `json:"date_created"` + RequiresSponsorship bool `json:"requires_sponsorship,omitempty"` + // TeamID string `json:"team_id,omitempty"` +} + +// SlackEventIcon is a struct for icons in ShareChannel events +type SlackEventIcon struct { + ImageDefault bool `json:"image_default,omitempty"` + Image34 string `json:"image_34,omitempty"` + Image44 string `json:"image_44,omitempty"` + Image68 string `json:"image_68,omitempty"` + Image88 string `json:"image_88,omitempty"` + Image102 string `json:"image_102,omitempty"` + Image132 string `json:"image_132,omitempty"` + Image230 string `json:"image_230,omitempty"` +} + +// SlackEventUser is a struct for users in ShareChannel events +type SlackEventUser struct { + ID string `json:"id"` + TeamID string `json:"team_id"` + Name string `json:"name"` + Updated int `json:"updated,omitempty"` + Profile *slack.UserProfile `json:"profile,omitempty"` + WhoCanShareContactCard string `json:"who_can_share_contact_card,omitempty"` +} + +// SharedChannel is a struct for shared channels in ShareChannel events +type SharedChannel struct { + ID string `json:"id"` + IsPrivate bool `json:"is_private"` + IsIm bool `json:"is_im"` + Name string `json:"name,omitempty"` +} + +// SharedInvite is a struct for shared invites in ShareChannel events +type SharedInvite struct { + ID string `json:"id"` + DateCreated int `json:"date_created"` + DateInvalid int `json:"date_invalid"` + InvitingTeam *SlackEventTeam `json:"inviting_team,omitempty"` + InvitingUser *SlackEventUser `json:"inviting_user,omitempty"` + RecipientEmail string `json:"recipient_email,omitempty"` + RecipientUserID string `json:"recipient_user_id,omitempty"` + IsSponsored bool `json:"is_sponsored,omitempty"` + IsExternalLimited bool `json:"is_external_limited,omitempty"` +} + +type ChannelHistoryChangedEvent struct { + Type string `json:"type"` + Latest string `json:"latest"` + Ts string `json:"ts"` + EventTs string `json:"event_ts"` +} + +type CommandsChangedEvent struct { + Type string `json:"type"` + EventTs string `json:"event_ts"` +} + +type DndUpdatedEvent struct { + Type string `json:"type"` + User string `json:"user"` + DndStatus struct { + DndEnabled bool `json:"dnd_enabled"` + NextDndStartTs int64 `json:"next_dnd_start_ts"` + NextDndEndTs int64 `json:"next_dnd_end_ts"` + SnoozeEnabled bool `json:"snooze_enabled"` + SnoozeEndtime int64 `json:"snooze_endtime"` + } `json:"dnd_status"` +} + +type DndUpdatedUserEvent struct { + Type string `json:"type"` + User string `json:"user"` + DndStatus struct { + DndEnabled bool `json:"dnd_enabled"` + NextDndStartTs int64 `json:"next_dnd_start_ts"` + NextDndEndTs int64 `json:"next_dnd_end_ts"` + } `json:"dnd_status"` +} + +type EmailDomainChangedEvent struct { + Type string `json:"type"` + EmailDomain string `json:"email_domain"` + EventTs string `json:"event_ts"` +} + +type GroupCloseEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` +} + +type GroupHistoryChangedEvent struct { + Type string `json:"type"` + Latest string `json:"latest"` + Ts string `json:"ts"` + EventTs string `json:"event_ts"` +} + +type GroupOpenEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` +} + +type ImCloseEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` +} + +type ImCreatedEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel struct { + ID string `json:"id"` + } `json:"channel"` +} + +type ImHistoryChangedEvent struct { + Type string `json:"type"` + Latest string `json:"latest"` + Ts string `json:"ts"` + EventTs string `json:"event_ts"` +} + +type ImOpenEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` +} + +type SubTeam struct { + ID string `json:"id"` + TeamID string `json:"team_id"` + IsUsergroup bool `json:"is_usergroup"` + Name string `json:"name"` + Description string `json:"description"` + Handle string `json:"handle"` + IsExternal bool `json:"is_external"` + DateCreate int64 `json:"date_create"` + DateUpdate int64 `json:"date_update"` + DateDelete int64 `json:"date_delete"` + AutoType string `json:"auto_type"` + CreatedBy string `json:"created_by"` + UpdatedBy string `json:"updated_by"` + DeletedBy string `json:"deleted_by"` + Prefs struct { + Channels []string `json:"channels"` + Groups []string `json:"groups"` + } `json:"prefs"` + Users []string `json:"users"` + UserCount int `json:"user_count"` +} + +type SubteamCreatedEvent struct { + Type string `json:"type"` + Subteam SubTeam `json:"subteam"` +} + +type SubteamMembersChangedEvent struct { + Type string `json:"type"` + SubteamID string `json:"subteam_id"` + TeamID string `json:"team_id"` + DatePreviousUpdate int `json:"date_previous_update"` + DateUpdate int64 `json:"date_update"` + AddedUsers []string `json:"added_users"` + AddedUsersCount string `json:"added_users_count"` + RemovedUsers []string `json:"removed_users"` + RemovedUsersCount string `json:"removed_users_count"` +} + +type SubteamSelfAddedEvent struct { + Type string `json:"type"` + SubteamID string `json:"subteam_id"` +} + +type SubteamSelfRemovedEvent struct { + Type string `json:"type"` + SubteamID string `json:"subteam_id"` +} + +type SubteamUpdatedEvent struct { + Type string `json:"type"` + Subteam SubTeam `json:"subteam"` +} + +type TeamDomainChangeEvent struct { + Type string `json:"type"` + URL string `json:"url"` + Domain string `json:"domain"` + TeamID string `json:"team_id"` +} + +type TeamRenameEvent struct { + Type string `json:"type"` + Name string `json:"name"` + TeamID string `json:"team_id"` +} + +type UserChangeEvent struct { + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` +} + +type AppDeletedEvent struct { + Type string `json:"type"` + AppID string `json:"app_id"` + AppName string `json:"app_name"` + AppOwnerID string `json:"app_owner_id"` + TeamID string `json:"team_id"` + TeamDomain string `json:"team_domain"` + EventTs string `json:"event_ts"` +} + +type AppInstalledEvent struct { + Type string `json:"type"` + AppID string `json:"app_id"` + AppName string `json:"app_name"` + AppOwnerID string `json:"app_owner_id"` + UserID string `json:"user_id"` + TeamID string `json:"team_id"` + TeamDomain string `json:"team_domain"` + EventTs string `json:"event_ts"` +} + +type AppRequestedEvent struct { + Type string `json:"type"` + AppRequest struct { + ID string `json:"id"` + App struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + HelpURL string `json:"help_url"` + PrivacyPolicyURL string `json:"privacy_policy_url"` + AppHomepageURL string `json:"app_homepage_url"` + AppDirectoryURL string `json:"app_directory_url"` + IsAppDirectoryApproved bool `json:"is_app_directory_approved"` + IsInternal bool `json:"is_internal"` + AdditionalInfo string `json:"additional_info"` + } `json:"app"` + PreviousResolution struct { + Status string `json:"status"` + Scopes []struct { + Name string `json:"name"` + Description string `json:"description"` + IsSensitive bool `json:"is_sensitive"` + TokenType string `json:"token_type"` + } `json:"scopes"` + } `json:"previous_resolution"` + User struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + } `json:"user"` + Team struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` + } `json:"team"` + Enterprise interface{} `json:"enterprise"` + Scopes []struct { + Name string `json:"name"` + Description string `json:"description"` + IsSensitive bool `json:"is_sensitive"` + TokenType string `json:"token_type"` + } `json:"scopes"` + Message string `json:"message"` + } `json:"app_request"` +} + +type AppUninstalledTeamEvent struct { + Type string `json:"type"` + AppID string `json:"app_id"` + AppName string `json:"app_name"` + AppOwnerID string `json:"app_owner_id"` + UserID string `json:"user_id"` + TeamID string `json:"team_id"` + TeamDomain string `json:"team_domain"` + EventTs string `json:"event_ts"` +} + +type CallRejectedEvent struct { + Token string `json:"token"` + TeamID string `json:"team_id"` + APIAppID string `json:"api_app_id"` + Event struct { + Type string `json:"type"` + CallID string `json:"call_id"` + UserID string `json:"user_id"` + ChannelID string `json:"channel_id"` + ExternalUniqueID string `json:"external_unique_id"` + } `json:"event"` + Type string `json:"type"` + EventID string `json:"event_id"` + AuthedUsers []string `json:"authed_users"` +} + +type ChannelSharedEvent struct { + Type string `json:"type"` + ConnectedTeamID string `json:"connected_team_id"` + Channel string `json:"channel"` + EventTs string `json:"event_ts"` +} + +type FileCreatedEvent struct { + Type string `json:"type"` + FileID string `json:"file_id"` + File struct { + ID string `json:"id"` + } `json:"file"` +} + +type FilePublicEvent struct { + Type string `json:"type"` + FileID string `json:"file_id"` + File struct { + ID string `json:"id"` + } `json:"file"` +} + +type FunctionExecutedEvent struct { + Type string `json:"type"` + Function struct { + ID string `json:"id"` + CallbackID string `json:"callback_id"` + Title string `json:"title"` + Description string `json:"description"` + Type string `json:"type"` + InputParameters []struct { + Type string `json:"type"` + Name string `json:"name"` + Description string `json:"description"` + Title string `json:"title"` + IsRequired bool `json:"is_required"` + } `json:"input_parameters"` + OutputParameters []struct { + Type string `json:"type"` + Name string `json:"name"` + Description string `json:"description"` + Title string `json:"title"` + IsRequired bool `json:"is_required"` + } `json:"output_parameters"` + AppID string `json:"app_id"` + DateCreated int64 `json:"date_created"` + DateUpdated int64 `json:"date_updated"` + DateDeleted int64 `json:"date_deleted"` + } `json:"function"` + Inputs map[string]string `json:"inputs"` + FunctionExecutionID string `json:"function_execution_id"` + WorkflowExecutionID string `json:"workflow_execution_id"` + EventTs string `json:"event_ts"` + BotAccessToken string `json:"bot_access_token"` +} + +type InviteRequestedEvent struct { + Type string `json:"type"` + InviteRequest struct { + ID string `json:"id"` + Email string `json:"email"` + DateCreated int64 `json:"date_created"` + RequesterIDs []string `json:"requester_ids"` + ChannelIDs []string `json:"channel_ids"` + InviteType string `json:"invite_type"` + RealName string `json:"real_name"` + DateExpire int64 `json:"date_expire"` + RequestReason string `json:"request_reason"` + Team struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` + } `json:"team"` + } `json:"invite_request"` +} + +type StarAddedEvent struct { + Type string `json:"type"` + User string `json:"user"` + Item struct { + } `json:"item"` + EventTS string `json:"event_ts"` +} + +type StarRemovedEvent struct { + Type string `json:"type"` + User string `json:"user"` + Item struct { + } `json:"item"` + EventTS string `json:"event_ts"` +} + +type UserHuddleChangedEvent struct { + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` +} + +type User struct { + ID string `json:"id"` + TeamID string `json:"team_id"` + Name string `json:"name"` + Deleted bool `json:"deleted"` + Color string `json:"color"` + RealName string `json:"real_name"` + TZ string `json:"tz"` + TZLabel string `json:"tz_label"` + TZOffset int `json:"tz_offset"` + Profile Profile `json:"profile"` + IsAdmin bool `json:"is_admin"` + IsOwner bool `json:"is_owner"` + IsPrimaryOwner bool `json:"is_primary_owner"` + IsRestricted bool `json:"is_restricted"` + IsUltraRestricted bool `json:"is_ultra_restricted"` + IsBot bool `json:"is_bot"` + IsAppUser bool `json:"is_app_user"` + Updated int64 `json:"updated"` + IsEmailConfirmed bool `json:"is_email_confirmed"` + WhoCanShareContactCard string `json:"who_can_share_contact_card"` + Locale string `json:"locale"` +} + +type Profile struct { + Title string `json:"title"` + Phone string `json:"phone"` + Skype string `json:"skype"` + RealName string `json:"real_name"` + RealNameNormalized string `json:"real_name_normalized"` + DisplayName string `json:"display_name"` + DisplayNameNormalized string `json:"display_name_normalized"` + Fields map[string]interface{} `json:"fields"` + StatusText string `json:"status_text"` + StatusEmoji string `json:"status_emoji"` + StatusEmojiDisplayInfo []interface{} `json:"status_emoji_display_info"` + StatusExpiration int `json:"status_expiration"` + AvatarHash string `json:"avatar_hash"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Image24 string `json:"image_24"` + Image32 string `json:"image_32"` + Image48 string `json:"image_48"` + Image72 string `json:"image_72"` + Image192 string `json:"image_192"` + Image512 string `json:"image_512"` + StatusTextCanonical string `json:"status_text_canonical"` + Team string `json:"team"` +} + +type UserStatusChangedEvent struct { + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` +} + +type Actor struct { + ID string `json:"id"` + Name string `json:"name"` + IsBot bool `json:"is_bot"` + TeamID string `json:"team_id"` + Timezone string `json:"timezone"` + RealName string `json:"real_name"` + DisplayName string `json:"display_name"` +} + +type TargetUser struct { + Email string `json:"email"` + InviteID string `json:"invite_id"` +} + +type TeamIcon struct { + Image34 string `json:"image_34"` + ImageDefault bool `json:"image_default"` +} + +type Team struct { + ID string `json:"id"` + Icon TeamIcon `json:"icon"` + Name string `json:"name"` + Domain string `json:"domain"` + IsVerified bool `json:"is_verified"` + DateCreated int64 `json:"date_created"` + AvatarBaseURL string `json:"avatar_base_url"` + RequiresSponsorship bool `json:"requires_sponsorship"` +} + +type SharedChannelInviteRequestedEvent struct { + Actor Actor `json:"actor"` + ChannelID string `json:"channel_id"` + EventType string `json:"event_type"` + ChannelName string `json:"channel_name"` + ChannelType string `json:"channel_type"` + TargetUsers []TargetUser `json:"target_users"` + TeamsInChannel []Team `json:"teams_in_channel"` + IsExternalLimited bool `json:"is_external_limited"` + ChannelDateCreated int64 `json:"channel_date_created"` + ChannelMessageLatestCounted int64 `json:"channel_message_latest_counted_timestamp"` +} + +type EventsAPIType string + +const ( + // AppMention is an Events API subscribable event + AppMention = EventsAPIType("app_mention") + // AppHomeOpened Your Slack app home was opened + AppHomeOpened = EventsAPIType("app_home_opened") + // AppUninstalled Your Slack app was uninstalled. + AppUninstalled = EventsAPIType("app_uninstalled") + // ChannelCreated is sent when a new channel is created. + ChannelCreated = EventsAPIType("channel_created") + // ChannelDeleted is sent when a channel is deleted. + ChannelDeleted = EventsAPIType("channel_deleted") + // ChannelArchive is sent when a channel is archived. + ChannelArchive = EventsAPIType("channel_archive") + // ChannelUnarchive is sent when a channel is unarchived. + ChannelUnarchive = EventsAPIType("channel_unarchive") + // ChannelLeft is sent when a channel is left. + ChannelLeft = EventsAPIType("channel_left") + // ChannelRename is sent when a channel is rename. + ChannelRename = EventsAPIType("channel_rename") + // ChannelIDChanged is sent when a channel identifier is changed. + ChannelIDChanged = EventsAPIType("channel_id_changed") + // GroupDeleted is sent when a group is deleted. + GroupDeleted = EventsAPIType("group_deleted") + // GroupArchive is sent when a group is archived. + GroupArchive = EventsAPIType("group_archive") + // GroupUnarchive is sent when a group is unarchived. + GroupUnarchive = EventsAPIType("group_unarchive") + // GroupLeft is sent when a group is left. + GroupLeft = EventsAPIType("group_left") + // GroupRename is sent when a group is renamed. + GroupRename = EventsAPIType("group_rename") + // FileChange is sent when a file is changed. + FileChange = EventsAPIType("file_change") + // FileDeleted is sent when a file is deleted. + FileDeleted = EventsAPIType("file_deleted") + // FileShared is sent when a file is shared. + FileShared = EventsAPIType("file_shared") + // FileUnshared is sent when a file is unshared. + FileUnshared = EventsAPIType("file_unshared") + // GridMigrationFinished An enterprise grid migration has finished on this workspace. + GridMigrationFinished = EventsAPIType("grid_migration_finished") + // GridMigrationStarted An enterprise grid migration has started on this workspace. + GridMigrationStarted = EventsAPIType("grid_migration_started") + // LinkShared A message was posted containing one or more links relevant to your application + LinkShared = EventsAPIType("link_shared") + // Message A message was posted to a channel, private channel (group), im, or mim + Message = EventsAPIType("message") + // MemberJoinedChannel is sent if a member joined a channel. + MemberJoinedChannel = EventsAPIType("member_joined_channel") + // MemberLeftChannel is sent if a member left a channel. + MemberLeftChannel = EventsAPIType("member_left_channel") + // PinAdded An item was pinned to a channel + PinAdded = EventsAPIType("pin_added") + // PinRemoved An item was unpinned from a channel + PinRemoved = EventsAPIType("pin_removed") + // ReactionAdded An reaction was added to a message + ReactionAdded = EventsAPIType("reaction_added") + // ReactionRemoved An reaction was removed from a message + ReactionRemoved = EventsAPIType("reaction_removed") + // TeamJoin A new user joined the workspace + TeamJoin = EventsAPIType("team_join") + // Slack connect app or bot invite received + SharedChannelInviteReceived = EventsAPIType("shared_channel_invite_received") + // Slack connect channel invite approved + SharedChannelInviteApproved = EventsAPIType("shared_channel_invite_approved") + // Slack connect channel invite declined + SharedChannelInviteDeclined = EventsAPIType("shared_channel_invite_declined") + // Slack connect channel invite accepted by an end user + SharedChannelInviteAccepted = EventsAPIType("shared_channel_invite_accepted") + // TokensRevoked APP's API tokes are revoked + TokensRevoked = EventsAPIType("tokens_revoked") + // EmojiChanged A custom emoji has been added or changed + EmojiChanged = EventsAPIType("emoji_changed") + // WorkflowStepExecute Happens, if a workflow step of your app is invoked + WorkflowStepExecute = EventsAPIType("workflow_step_execute") + // MessageMetadataPosted A message with metadata was posted + MessageMetadataPosted = EventsAPIType("message_metadata_posted") + // MessageMetadataUpdated A message with metadata was updated + MessageMetadataUpdated = EventsAPIType("message_metadata_updated") + // MessageMetadataDeleted A message with metadata was deleted + MessageMetadataDeleted = EventsAPIType("message_metadata_deleted") + // TeamAccessGranted is sent if access to teams was granted for your org-wide app. + TeamAccessGranted = EventsAPIType("team_access_granted") + // TeamAccessRevoked is sent if access to teams was revoked for your org-wide app. + TeamAccessRevoked = EventsAPIType("team_access_revoked") + // UserProfileChanged is sent if a user's profile information has changed. + UserProfileChanged = EventsAPIType("user_profile_changed") + // ChannelHistoryChanged The history of a channel changed + ChannelHistoryChanged = EventsAPIType("channel_history_changed") + // CommandsChanged A command was changed + CommandsChanged = EventsAPIType("commands_changed") + // DndUpdated Do Not Disturb settings were updated + DndUpdated = EventsAPIType("dnd_updated") + // DndUpdatedUser Do Not Disturb settings for a user were updated + DndUpdatedUser = EventsAPIType("dnd_updated_user") + // EmailDomainChanged The email domain changed + EmailDomainChanged = EventsAPIType("email_domain_changed") + // GroupClose A group was closed + GroupClose = EventsAPIType("group_close") + // GroupHistoryChanged The history of a group changed + GroupHistoryChanged = EventsAPIType("group_history_changed") + // GroupOpen A group was opened + GroupOpen = EventsAPIType("group_open") + // ImClose An instant message channel was closed + ImClose = EventsAPIType("im_close") + // ImCreated An instant message channel was created + ImCreated = EventsAPIType("im_created") + // ImHistoryChanged The history of an instant message channel changed + ImHistoryChanged = EventsAPIType("im_history_changed") + // ImOpen An instant message channel was opened + ImOpen = EventsAPIType("im_open") + // SubteamCreated A subteam was created + SubteamCreated = EventsAPIType("subteam_created") + // SubteamMembersChanged The members of a subteam changed + SubteamMembersChanged = EventsAPIType("subteam_members_changed") + // SubteamSelfAdded The current user was added to a subteam + SubteamSelfAdded = EventsAPIType("subteam_self_added") + // SubteamSelfRemoved The current user was removed from a subteam + SubteamSelfRemoved = EventsAPIType("subteam_self_removed") + // SubteamUpdated A subteam was updated + SubteamUpdated = EventsAPIType("subteam_updated") + // TeamDomainChange The team's domain changed + TeamDomainChange = EventsAPIType("team_domain_change") + // TeamRename The team was renamed + TeamRename = EventsAPIType("team_rename") + // UserChange A user object has changed + UserChange = EventsAPIType("user_change") + // AppDeleted is an event when an app is deleted from a workspace + AppDeleted = EventsAPIType("app_deleted") + // AppInstalled is an event when an app is installed to a workspace + AppInstalled = EventsAPIType("app_installed") + // AppRequested is an event when a user requests to install an app to a workspace + AppRequested = EventsAPIType("app_requested") + // AppUninstalledTeam is an event when an app is uninstalled from a team + AppUninstalledTeam = EventsAPIType("app_uninstalled_team") + // CallRejected is an event when a Slack call is rejected + CallRejected = EventsAPIType("call_rejected") + // ChannelShared is an event when a channel is shared with another workspace + ChannelShared = EventsAPIType("channel_shared") + // FileCreated is an event when a file is created in a workspace + FileCreated = EventsAPIType("file_created") + // FilePublic is an event when a file is made public in a workspace + FilePublic = EventsAPIType("file_public") + // FunctionExecuted is an event when a Slack function is executed + FunctionExecuted = EventsAPIType("function_executed") + // InviteRequested is an event when a user requests an invite to a workspace + InviteRequested = EventsAPIType("invite_requested") + // SharedChannelInviteRequested is an event when an invitation to share a channel is requested + SharedChannelInviteRequested = EventsAPIType("shared_channel_invite_requested") + // StarAdded is an event when a star is added to a message or file + StarAdded = EventsAPIType("star_added") + // StarRemoved is an event when a star is removed from a message or file + StarRemoved = EventsAPIType("star_removed") + // UserHuddleChanged is an event when a user's huddle status changes + UserHuddleChanged = EventsAPIType("user_huddle_changed") + // UserStatusChanged is an event when a user's status changes + UserStatusChanged = EventsAPIType("user_status_changed") +) + +// EventsAPIInnerEventMapping maps INNER Event API events to their corresponding struct +// implementations. The structs should be instances of the unmarshalling +// target for the matching event type. +var EventsAPIInnerEventMapping = map[EventsAPIType]interface{}{ + AppMention: AppMentionEvent{}, + AppHomeOpened: AppHomeOpenedEvent{}, + AppUninstalled: AppUninstalledEvent{}, + ChannelCreated: ChannelCreatedEvent{}, + ChannelDeleted: ChannelDeletedEvent{}, + ChannelArchive: ChannelArchiveEvent{}, + ChannelUnarchive: ChannelUnarchiveEvent{}, + ChannelLeft: ChannelLeftEvent{}, + ChannelRename: ChannelRenameEvent{}, + ChannelIDChanged: ChannelIDChangedEvent{}, + FileChange: FileChangeEvent{}, + FileDeleted: FileDeletedEvent{}, + FileShared: FileSharedEvent{}, + FileUnshared: FileUnsharedEvent{}, + GroupDeleted: GroupDeletedEvent{}, + GroupArchive: GroupArchiveEvent{}, + GroupUnarchive: GroupUnarchiveEvent{}, + GroupLeft: GroupLeftEvent{}, + GroupRename: GroupRenameEvent{}, + GridMigrationFinished: GridMigrationFinishedEvent{}, + GridMigrationStarted: GridMigrationStartedEvent{}, + LinkShared: LinkSharedEvent{}, + Message: MessageEvent{}, + MemberJoinedChannel: MemberJoinedChannelEvent{}, + MemberLeftChannel: MemberLeftChannelEvent{}, + PinAdded: PinAddedEvent{}, + PinRemoved: PinRemovedEvent{}, + ReactionAdded: ReactionAddedEvent{}, + ReactionRemoved: ReactionRemovedEvent{}, + SharedChannelInviteApproved: SharedChannelInviteApprovedEvent{}, + SharedChannelInviteAccepted: SharedChannelInviteAcceptedEvent{}, + SharedChannelInviteDeclined: SharedChannelInviteDeclinedEvent{}, + SharedChannelInviteReceived: SharedChannelInviteReceivedEvent{}, + TeamJoin: TeamJoinEvent{}, + TokensRevoked: TokensRevokedEvent{}, + EmojiChanged: EmojiChangedEvent{}, + WorkflowStepExecute: WorkflowStepExecuteEvent{}, + MessageMetadataPosted: MessageMetadataPostedEvent{}, + MessageMetadataUpdated: MessageMetadataUpdatedEvent{}, + MessageMetadataDeleted: MessageMetadataDeletedEvent{}, + TeamAccessGranted: TeamAccessGrantedEvent{}, + TeamAccessRevoked: TeamAccessRevokedEvent{}, + UserProfileChanged: UserProfileChangedEvent{}, + ChannelHistoryChanged: ChannelHistoryChangedEvent{}, + DndUpdated: DndUpdatedEvent{}, + DndUpdatedUser: DndUpdatedUserEvent{}, + EmailDomainChanged: EmailDomainChangedEvent{}, + GroupClose: GroupCloseEvent{}, + GroupHistoryChanged: GroupHistoryChangedEvent{}, + GroupOpen: GroupOpenEvent{}, + ImClose: ImCloseEvent{}, + ImCreated: ImCreatedEvent{}, + ImHistoryChanged: ImHistoryChangedEvent{}, + ImOpen: ImOpenEvent{}, + SubteamCreated: SubteamCreatedEvent{}, + SubteamMembersChanged: SubteamMembersChangedEvent{}, + SubteamSelfAdded: SubteamSelfAddedEvent{}, + SubteamSelfRemoved: SubteamSelfRemovedEvent{}, + SubteamUpdated: SubteamUpdatedEvent{}, + TeamDomainChange: TeamDomainChangeEvent{}, + TeamRename: TeamRenameEvent{}, + UserChange: UserChangeEvent{}, + AppDeleted: AppDeletedEvent{}, + AppInstalled: AppInstalledEvent{}, + AppRequested: AppRequestedEvent{}, + AppUninstalledTeam: AppUninstalledTeamEvent{}, + CallRejected: CallRejectedEvent{}, + ChannelShared: ChannelSharedEvent{}, + FileCreated: FileCreatedEvent{}, + FilePublic: FilePublicEvent{}, + FunctionExecuted: FunctionExecutedEvent{}, + InviteRequested: InviteRequestedEvent{}, + SharedChannelInviteRequested: SharedChannelInviteRequestedEvent{}, + StarAdded: StarAddedEvent{}, + StarRemoved: StarRemovedEvent{}, + UserHuddleChanged: UserHuddleChangedEvent{}, + UserStatusChanged: UserStatusChangedEvent{}, +} diff --git a/vendor/github.com/slack-go/slack/slackevents/outer_events.go b/vendor/github.com/slack-go/slack/slackevents/outer_events.go new file mode 100644 index 0000000000..bcc85f3314 --- /dev/null +++ b/vendor/github.com/slack-go/slack/slackevents/outer_events.go @@ -0,0 +1,72 @@ +// outer_events.go provides EventsAPI particular outer events + +package slackevents + +import ( + "encoding/json" +) + +// EventsAPIEvent is the base EventsAPIEvent +type EventsAPIEvent struct { + Token string `json:"token"` + TeamID string `json:"team_id"` + Type string `json:"type"` + APIAppID string `json:"api_app_id"` + EnterpriseID string `json:"enterprise_id"` + Data interface{} + InnerEvent EventsAPIInnerEvent +} + +// EventsAPIURLVerificationEvent received when configuring a EventsAPI driven app +type EventsAPIURLVerificationEvent struct { + Token string `json:"token"` + Challenge string `json:"challenge"` + Type string `json:"type"` +} + +// ChallengeResponse is a response to a EventsAPIEvent URLVerification challenge +type ChallengeResponse struct { + Challenge string +} + +// EventsAPICallbackEvent is the main (outer) EventsAPI event. +type EventsAPICallbackEvent struct { + Type string `json:"type"` + Token string `json:"token"` + TeamID string `json:"team_id"` + APIAppID string `json:"api_app_id"` + EnterpriseID string `json:"enterprise_id"` + InnerEvent *json.RawMessage `json:"event"` + AuthedUsers []string `json:"authed_users"` + AuthedTeams []string `json:"authed_teams"` + EventID string `json:"event_id"` + EventTime int `json:"event_time"` + EventContext string `json:"event_context"` +} + +// EventsAPIAppRateLimited indicates your app's event subscriptions are being rate limited +type EventsAPIAppRateLimited struct { + Type string `json:"type"` + Token string `json:"token"` + TeamID string `json:"team_id"` + MinuteRateLimited int `json:"minute_rate_limited"` + APIAppID string `json:"api_app_id"` +} + +const ( + // CallbackEvent is the "outer" event of an EventsAPI event. + CallbackEvent = "event_callback" + // URLVerification is an event used when configuring your EventsAPI app + URLVerification = "url_verification" + // AppRateLimited indicates your app's event subscriptions are being rate limited + AppRateLimited = "app_rate_limited" +) + +// EventsAPIEventMap maps OUTER Event API events to their corresponding struct +// implementations. The structs should be instances of the unmarshalling +// target for the matching event type. +var EventsAPIEventMap = map[string]interface{}{ + CallbackEvent: EventsAPICallbackEvent{}, + URLVerification: EventsAPIURLVerificationEvent{}, + AppRateLimited: EventsAPIAppRateLimited{}, +} diff --git a/vendor/github.com/slack-go/slack/slackevents/parsers.go b/vendor/github.com/slack-go/slack/slackevents/parsers.go new file mode 100644 index 0000000000..9e8c22b7f3 --- /dev/null +++ b/vendor/github.com/slack-go/slack/slackevents/parsers.go @@ -0,0 +1,284 @@ +package slackevents + +import ( + "crypto/subtle" + "encoding/json" + "errors" + "fmt" + "reflect" + + "github.com/slack-go/slack" +) + +// eventsMap checks both slack.EventsMapping and +// and slackevents.EventsAPIInnerEventMapping. If the event +// exists, returns the unmarshalled struct instance of +// target for the matching event type. +// TODO: Consider moving all events into its own package? +func eventsMap(t string) (interface{}, bool) { + // Must parse EventsAPI FIRST as both RTM and EventsAPI + // have a type: "Message" event. + // TODO: Handle these cases more explicitly. + v, exists := EventsAPIInnerEventMapping[EventsAPIType(t)] + if exists { + return v, exists + } + v, exists = slack.EventMapping[t] + if exists { + return v, exists + } + return v, exists +} + +func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) { + e := &EventsAPIEvent{} + err := json.Unmarshal(rawE, e) + if err != nil { + return EventsAPIEvent{ + "", + "", + "unmarshalling_error", + "", + "", + &slack.UnmarshallingErrorEvent{ErrorObj: err}, + EventsAPIInnerEvent{}, + }, err + } + if e.Type == CallbackEvent { + cbEvent := &EventsAPICallbackEvent{} + err = json.Unmarshal(rawE, cbEvent) + if err != nil { + return EventsAPIEvent{ + "", + "", + "unmarshalling_error", + "", + "", + &slack.UnmarshallingErrorEvent{ErrorObj: err}, + EventsAPIInnerEvent{}, + }, err + } + return EventsAPIEvent{ + e.Token, + e.TeamID, + e.Type, + e.APIAppID, + e.EnterpriseID, + cbEvent, + EventsAPIInnerEvent{}, + }, nil + } + urlVE := &EventsAPIURLVerificationEvent{} + err = json.Unmarshal(rawE, urlVE) + if err != nil { + return EventsAPIEvent{ + "", + "", + "unmarshalling_error", + "", + "", + &slack.UnmarshallingErrorEvent{ErrorObj: err}, + EventsAPIInnerEvent{}, + }, err + } + return EventsAPIEvent{ + e.Token, + e.TeamID, + e.Type, + e.APIAppID, + e.EnterpriseID, + urlVE, + EventsAPIInnerEvent{}, + }, nil +} + +func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) { + iE := &slack.Event{} + rawInnerJSON := e.InnerEvent + err := json.Unmarshal(*rawInnerJSON, iE) + if err != nil { + return EventsAPIEvent{ + e.Token, + e.TeamID, + "unmarshalling_error", + e.APIAppID, + e.EnterpriseID, + &slack.UnmarshallingErrorEvent{ErrorObj: err}, + EventsAPIInnerEvent{}, + }, err + } + v, exists := eventsMap(iE.Type) + if !exists { + return EventsAPIEvent{ + e.Token, + e.TeamID, + iE.Type, + e.APIAppID, + e.EnterpriseID, + nil, + EventsAPIInnerEvent{}, + }, fmt.Errorf("inner Event does not exist! %s", iE.Type) + } + t := reflect.TypeOf(v) + recvEvent := reflect.New(t).Interface() + err = json.Unmarshal(*rawInnerJSON, recvEvent) + if err != nil { + return EventsAPIEvent{ + e.Token, + e.TeamID, + "unmarshalling_error", + e.APIAppID, + e.EnterpriseID, + &slack.UnmarshallingErrorEvent{ErrorObj: err}, + EventsAPIInnerEvent{}, + }, err + } + return EventsAPIEvent{ + e.Token, + e.TeamID, + e.Type, + e.APIAppID, + e.EnterpriseID, + e, + EventsAPIInnerEvent{iE.Type, recvEvent}, + }, nil +} + +type Config struct { + VerificationToken string + TokenVerified bool +} + +type Option func(cfg *Config) + +type verifier interface { + Verify(token string) bool +} + +func OptionVerifyToken(v verifier) Option { + return func(cfg *Config) { + cfg.TokenVerified = v.Verify(cfg.VerificationToken) + } +} + +// OptionNoVerifyToken skips the check of the Slack verification token +func OptionNoVerifyToken() Option { + return func(cfg *Config) { + cfg.TokenVerified = true + } +} + +type TokenComparator struct { + VerificationToken string +} + +func (c TokenComparator) Verify(t string) bool { + return subtle.ConstantTimeCompare([]byte(c.VerificationToken), []byte(t)) == 1 +} + +// ParseEvent parses the outer and inner events (if applicable) of an events +// api event returning a EventsAPIEvent type. If the event is a url_verification event, +// the inner event is empty. +func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error) { + e, err := parseOuterEvent(rawEvent) + if err != nil { + return EventsAPIEvent{}, err + } + + cfg := &Config{} + cfg.VerificationToken = e.Token + for _, opt := range opts { + opt(cfg) + } + + if !cfg.TokenVerified { + return EventsAPIEvent{}, errors.New("invalid verification token") + } + + if e.Type == CallbackEvent { + cbEvent := e.Data.(*EventsAPICallbackEvent) + innerEvent, err := parseInnerEvent(cbEvent) + if err != nil { + err := fmt.Errorf("EventsAPI Error parsing inner event: %s, %s", innerEvent.Type, err) + return EventsAPIEvent{ + "", + "", + "unmarshalling_error", + "", + "", + &slack.UnmarshallingErrorEvent{ErrorObj: err}, + EventsAPIInnerEvent{}, + }, err + } + return innerEvent, nil + } + + if e.Type == AppRateLimited { + appRateLimitedEvent := &EventsAPIAppRateLimited{} + err = json.Unmarshal(rawEvent, appRateLimitedEvent) + if err != nil { + return EventsAPIEvent{ + "", + "", + "unmarshalling_error", + "", + "", + &slack.UnmarshallingErrorEvent{ErrorObj: err}, + EventsAPIInnerEvent{}, + }, err + } + return EventsAPIEvent{ + e.Token, + e.TeamID, + e.Type, + e.APIAppID, + e.EnterpriseID, + appRateLimitedEvent, + EventsAPIInnerEvent{}, + }, nil + } + + urlVerificationEvent := &EventsAPIURLVerificationEvent{} + err = json.Unmarshal(rawEvent, urlVerificationEvent) + if err != nil { + return EventsAPIEvent{ + "", + "", + "unmarshalling_error", + "", + "", + &slack.UnmarshallingErrorEvent{ErrorObj: err}, + EventsAPIInnerEvent{}, + }, err + } + return EventsAPIEvent{ + e.Token, + e.TeamID, + e.Type, + e.APIAppID, + e.EnterpriseID, + urlVerificationEvent, + EventsAPIInnerEvent{}, + }, nil +} + +func ParseActionEvent(payloadString string, opts ...Option) (MessageAction, error) { + byteString := []byte(payloadString) + action := MessageAction{} + err := json.Unmarshal(byteString, &action) + if err != nil { + return MessageAction{}, errors.New("MessageAction unmarshalling failed") + } + + cfg := &Config{} + cfg.VerificationToken = action.Token + for _, opt := range opts { + opt(cfg) + } + + if !cfg.TokenVerified { + return MessageAction{}, errors.New("invalid verification token") + } else { + return action, nil + } +} diff --git a/vendor/github.com/slack-go/slack/socketmode/client.go b/vendor/github.com/slack-go/slack/socketmode/client.go new file mode 100644 index 0000000000..8fd0b9871c --- /dev/null +++ b/vendor/github.com/slack-go/slack/socketmode/client.go @@ -0,0 +1,63 @@ +package socketmode + +import ( + "encoding/json" + "time" + + "github.com/slack-go/slack" + + "github.com/gorilla/websocket" +) + +type ConnectedEvent struct { + ConnectionCount int // 1 = first time, 2 = second time + Info *slack.SocketModeConnection +} + +type DebugInfo struct { + // Host is the name of the host name on the Slack end, that can be something like `applink-7fc4fdbb64-4x5xq` + Host string `json:"host"` + + // `hello` type only + BuildNumber int `json:"build_number"` + ApproximateConnectionTime int `json:"approximate_connection_time"` +} + +type ConnectionInfo struct { + AppID string `json:"app_id"` +} + +type SocketModeMessagePayload struct { + Event json.RawMessage `json:"event"` +} + +// Client is a Socket Mode client that allows programs to use [Events API](https://api.slack.com/events-api) +// and [interactive components](https://api.slack.com/interactivity) over WebSocket. +// Please see [Intro to Socket Mode](https://api.slack.com/apis/connections/socket) for more information +// on Socket Mode. +// +// The implementation is highly inspired by https://www.npmjs.com/package/@slack/socket-mode, +// but the structure and the design has been adapted as much as possible to that of our RTM client for consistency +// within the library. +// +// You can instantiate the socket mode client with +// Client's New() and call Run() to start it. Please see examples/socketmode for the usage. +type Client struct { + // Client is the main API, embedded + slack.Client + + // maxPingInterval is the maximum duration elapsed after the last WebSocket PING sent from Slack + // until Client considers the WebSocket connection is dead and needs to be reopened. + maxPingInterval time.Duration + + // Connection life-cycle + Events chan Event + socketModeResponses chan *Response + + // dialer is a gorilla/websocket Dialer. If nil, use the default + // Dialer. + dialer *websocket.Dialer + + debug bool + log ilogger +} diff --git a/vendor/github.com/slack-go/slack/socketmode/event.go b/vendor/github.com/slack-go/slack/socketmode/event.go new file mode 100644 index 0000000000..5ae434a704 --- /dev/null +++ b/vendor/github.com/slack-go/slack/socketmode/event.go @@ -0,0 +1,30 @@ +package socketmode + +import "encoding/json" + +// Event is the event sent to the consumer of Client +type Event struct { + Type EventType + Data interface{} + + // Request is the json-decoded raw WebSocket message that is received via the Slack Socket Mode + // WebSocket connection. + Request *Request +} + +type ErrorBadMessage struct { + Cause error + Message json.RawMessage +} + +type ErrorWriteFailed struct { + Cause error + Response *Response +} + +type errorRequestedDisconnect struct { +} + +func (e errorRequestedDisconnect) Error() string { + return "disconnection requested: Slack requested us to disconnect" +} diff --git a/vendor/github.com/slack-go/slack/socketmode/log.go b/vendor/github.com/slack-go/slack/socketmode/log.go new file mode 100644 index 0000000000..9f3b7f690c --- /dev/null +++ b/vendor/github.com/slack-go/slack/socketmode/log.go @@ -0,0 +1,51 @@ +package socketmode + +import "fmt" + +// TODO merge logger, ilogger, and internalLogger with the top-level package's equivalents + +// logger is a logger interface compatible with both stdlib and some +// 3rd party loggers. +type logger interface { + Output(int, string) error +} + +// ilogger represents the internal logging api we use. +type ilogger interface { + logger + Print(...interface{}) + Printf(string, ...interface{}) + Println(...interface{}) +} + +// internalLog implements the additional methods used by our internal logging. +type internalLog struct { + logger +} + +// Println replicates the behaviour of the standard logger. +func (t internalLog) Println(v ...interface{}) { + t.Output(2, fmt.Sprintln(v...)) +} + +// Printf replicates the behaviour of the standard logger. +func (t internalLog) Printf(format string, v ...interface{}) { + t.Output(2, fmt.Sprintf(format, v...)) +} + +// Print replicates the behaviour of the standard logger. +func (t internalLog) Print(v ...interface{}) { + t.Output(2, fmt.Sprint(v...)) +} + +func (smc *Client) Debugf(format string, v ...interface{}) { + if smc.debug { + smc.log.Output(2, fmt.Sprintf(format, v...)) + } +} + +func (smc *Client) Debugln(v ...interface{}) { + if smc.debug { + smc.log.Output(2, fmt.Sprintln(v...)) + } +} diff --git a/vendor/github.com/slack-go/slack/socketmode/request.go b/vendor/github.com/slack-go/slack/socketmode/request.go new file mode 100644 index 0000000000..254c8c49e6 --- /dev/null +++ b/vendor/github.com/slack-go/slack/socketmode/request.go @@ -0,0 +1,38 @@ +package socketmode + +import "encoding/json" + +// Request maps to the content of each WebSocket message received via a Socket Mode WebSocket connection +// +// We call this a "request" rather than e.g. a WebSocket message or an Socket Mode "event" following python-slack-sdk: +// +// https://github.com/slackapi/python-slack-sdk/blob/3f1c4c6e27bf7ee8af57699b2543e6eb7848bcf9/slack_sdk/socket_mode/request.py#L6 +// +// We know that node-slack-sdk calls it an "event", that makes it hard for us to distinguish our client's own event +// that wraps both internal events and Socket Mode "events", vs node-slack-sdk's is for the latter only. +// +// https://github.com/slackapi/node-slack-sdk/blob/main/packages/socket-mode/src/SocketModeClient.ts#L537 +type Request struct { + Type string `json:"type"` + + // `hello` type only + NumConnections int `json:"num_connections"` + ConnectionInfo ConnectionInfo `json:"connection_info"` + + // `disconnect` type only + + // Reason can be "warning" or else + Reason string `json:"reason"` + + // `hello` and `disconnect` types only + DebugInfo DebugInfo `json:"debug_info"` + + // `events_api` type only + EnvelopeID string `json:"envelope_id"` + // TODO Can it really be a non-object type? + // See https://github.com/slackapi/python-slack-sdk/blob/3f1c4c6e27bf7ee8af57699b2543e6eb7848bcf9/slack_sdk/socket_mode/request.py#L26-L31 + Payload json.RawMessage `json:"payload"` + AcceptsResponsePayload bool `json:"accepts_response_payload"` + RetryAttempt int `json:"retry_attempt"` + RetryReason string `json:"retry_reason"` +} diff --git a/vendor/github.com/slack-go/slack/socketmode/response.go b/vendor/github.com/slack-go/slack/socketmode/response.go new file mode 100644 index 0000000000..5c7bfabcfa --- /dev/null +++ b/vendor/github.com/slack-go/slack/socketmode/response.go @@ -0,0 +1,6 @@ +package socketmode + +type Response struct { + EnvelopeID string `json:"envelope_id"` + Payload interface{} `json:"payload,omitempty"` +} diff --git a/vendor/github.com/slack-go/slack/socketmode/socket_mode_managed_conn.go b/vendor/github.com/slack-go/slack/socketmode/socket_mode_managed_conn.go new file mode 100644 index 0000000000..b94456f49b --- /dev/null +++ b/vendor/github.com/slack-go/slack/socketmode/socket_mode_managed_conn.go @@ -0,0 +1,633 @@ +package socketmode + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" + + "github.com/slack-go/slack" + "github.com/slack-go/slack/internal/backoff" + "github.com/slack-go/slack/internal/timex" + "github.com/slack-go/slack/slackevents" +) + +// Run is a blocking function that connects the Slack Socket Mode API and handles all incoming +// requests and outgoing responses. +// +// The consumer of the Client and this function should read the Client.Events channel to receive +// `socketmode.Event`s that includes the client-specific events that may or may not wrap Socket Mode requests. +// +// Note that this function automatically reconnect on requested by Slack through a `disconnect` message. +// This function exists with an error only when a reconnection is failued due to some reason. +// If you want to retry even on reconnection failure, you'd need to write your own wrapper for this function +// to do so. +func (smc *Client) Run() error { + return smc.RunContext(context.TODO()) +} + +// RunContext is a blocking function that connects the Slack Socket Mode API and handles all incoming +// requests and outgoing responses. +// +// The consumer of the Client and this function should read the Client.Events channel to receive +// `socketmode.Event`s that includes the client-specific events that may or may not wrap Socket Mode requests. +// +// Note that this function automatically reconnect on requested by Slack through a `disconnect` message. +// This function exists with an error only when a reconnection is failued due to some reason. +// If you want to retry even on reconnection failure, you'd need to write your own wrapper for this function +// to do so. +func (smc *Client) RunContext(ctx context.Context) error { + for connectionCount := 0; ; connectionCount++ { + if err := smc.run(ctx, connectionCount); err != nil { + return err + } + + // Continue and run the loop again to reconnect + } +} + +func (smc *Client) run(ctx context.Context, connectionCount int) error { + messages := make(chan json.RawMessage, 1) + + pingChan := make(chan time.Time, 1) + pingHandler := func(_ string) error { + select { + case pingChan <- time.Now(): + default: + } + + return nil + } + + // Start trying to connect + // the returned err is already passed onto the Events channel + // + // We also configures an additional ping handler for the deadmanTimer that triggers a timeout when + // Slack did not send us WebSocket PING for more than Client.maxPingInterval. + // We can use `<-smc.pingTimeout.C` to wait for the timeout. + info, conn, err := smc.connect(ctx, connectionCount, pingHandler) + if err != nil { + // when the connection is unsuccessful its fatal, and we need to bail out. + smc.Debugf("Failed to connect with Socket Mode on try %d: %s", connectionCount, err) + + return err + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + smc.sendEvent(ctx, newEvent(EventTypeConnected, &ConnectedEvent{ + ConnectionCount: connectionCount, + Info: info, + })) + + smc.Debugf("WebSocket connection succeeded on try %d", connectionCount) + + // We're now connected so we can set up listeners + + wg := new(sync.WaitGroup) + // sendErr relies on the buffer of 1 here + errc := make(chan error, 1) + sendErr := func(err error) { + select { + case errc <- err: + default: + } + } + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + // The response sender sends Socket Mode responses over the WebSocket conn + if err := smc.runResponseSender(ctx, conn); err != nil { + sendErr(err) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + // The handler reads Socket Mode requests, and enqueues responses for sending by the response sender + if err := smc.runRequestHandler(ctx, messages); err != nil { + sendErr(err) + } + }() + + go func() { + defer cancel() + // We close messages here as it is the producer for the channel. + defer close(messages) + + // The receiver reads WebSocket messages, and enqueues parsed Socket Mode requests to be handled by + // the request handler + if err := smc.runMessageReceiver(ctx, conn, messages); err != nil { + sendErr(err) + } + }() + + wg.Add(1) + go func(pingInterval time.Duration) { + defer wg.Done() + defer func() { + // Detect when the connection is dead and try close connection. + if err := conn.Close(); err != nil { + smc.Debugf("Failed to close connection: %v", err) + } + }() + + done := ctx.Done() + var lastPing time.Time + + // More efficient than constantly resetting a timer w/ Stop+Reset + ticker := time.NewTicker(pingInterval) + defer ticker.Stop() + + for { + select { + case <-done: + return + + case lastPing = <-pingChan: + // This case gets the time of the last ping. + // If this case never fires then the pingHandler was never called + // in which case lastPing is the zero time.Time value, and will 'fail' + // the next tick, causing us to exit. + + case now := <-ticker.C: + // Our last ping is older than our interval + if now.Sub(lastPing) > pingInterval { + sendErr(errors.New("ping timeout: Slack did not send us WebSocket PING for more than Client.maxInterval")) + + cancel() + return + } + } + } + }(smc.maxPingInterval) + + wg.Wait() + + select { + case err = <-errc: + // Get buffered error + default: + // Or nothing if they all exited nil + } + + if errors.Is(err, context.Canceled) { + return err + } + + // wg.Wait() finishes only after any of the above go routines finishes and cancels the + // context, allowing the other threads to shut down gracefully. + // Also, we can expect our (first)err to be not nil, as goroutines can finish only on error. + smc.Debugf("Reconnecting due to %v", err) + + return nil +} + +// connect attempts to connect to the slack websocket API. It handles any +// errors that occur while connecting and will return once a connection +// has been successfully opened. +func (smc *Client) connect(ctx context.Context, connectionCount int, additionalPingHandler func(string) error) (*slack.SocketModeConnection, *websocket.Conn, error) { + const ( + errInvalidAuth = "invalid_auth" + errInactiveAccount = "account_inactive" + errMissingAuthToken = "not_authed" + errTokenRevoked = "token_revoked" + ) + + // used to provide exponential backoff wait time with jitter before trying + // to connect to slack again + boff := &backoff.Backoff{ + Max: 5 * time.Minute, + } + + for { + var ( + backoff time.Duration + ) + + // send connecting event + smc.sendEvent(ctx, newEvent(EventTypeConnecting, &slack.ConnectingEvent{ + Attempt: boff.Attempts() + 1, + ConnectionCount: connectionCount, + })) + + // attempt to start the connection + info, conn, err := smc.openAndDial(ctx, additionalPingHandler) + if err == nil { + return info, conn, nil + } + + // check for fatal errors + switch err.Error() { + case errInvalidAuth, errInactiveAccount, errMissingAuthToken, errTokenRevoked: + smc.Debugf("invalid auth when connecting with SocketMode: %s", err) + return nil, nil, err + default: + } + + var ( + actual slack.StatusCodeError + rlError *slack.RateLimitedError + ) + + if errors.As(err, &actual) && actual.Code == http.StatusNotFound { + smc.Debugf("invalid auth when connecting with Socket Mode: %s", err) + smc.sendEvent(ctx, newEvent(EventTypeInvalidAuth, &slack.InvalidAuthEvent{})) + + return nil, nil, err + } else if errors.As(err, &rlError) { + backoff = rlError.RetryAfter + } + + // If we check for errors.Is(err, context.Canceled) here and + // return early then we don't send the Event below that some users + // may already rely on; ie a behavior change. + + backoff = timex.Max(backoff, boff.Duration()) + // any other errors are treated as recoverable and we try again after + // sending the event along the Events channel + smc.sendEvent(ctx, newEvent(EventTypeConnectionError, &slack.ConnectionErrorEvent{ + Attempt: boff.Attempts(), + Backoff: backoff, + ErrorObj: err, + })) + + // get time we should wait before attempting to connect again + smc.Debugf("reconnection %d failed: %s reconnecting in %v\n", boff.Attempts(), err, backoff) + + // wait for one of the following to occur, + // backoff duration has elapsed, disconnectCh is signalled, or + // the smc finishes disconnecting. + timer := time.NewTimer(backoff) + select { + case <-timer.C: // retry after the backoff. + case <-ctx.Done(): + timer.Stop() + return nil, nil, ctx.Err() + } + } +} + +// openAndDial attempts to open a Socket Mode connection and dial to the connection endpoint using WebSocket. +// It returns the full information returned by the "apps.connections.open" method on the +// Slack API. +func (smc *Client) openAndDial(ctx context.Context, additionalPingHandler func(string) error) (info *slack.SocketModeConnection, _ *websocket.Conn, err error) { + var ( + url string + ) + + smc.Debugf("Starting SocketMode") + info, url, err = smc.OpenContext(ctx) + + if err != nil { + smc.Debugf("Failed to start or connect with SocketMode: %s", err) + return nil, nil, err + } + + smc.Debugf("Dialing to websocket on url %s", url) + // Only use HTTPS for connections to prevent MITM attacks on the connection. + upgradeHeader := http.Header{} + upgradeHeader.Add("Origin", "https://api.slack.com") + dialer := websocket.DefaultDialer + if smc.dialer != nil { + dialer = smc.dialer + } + conn, _, err := dialer.DialContext(ctx, url, upgradeHeader) + if err != nil { + smc.Debugf("Failed to dial to the websocket: %s", err) + return nil, nil, err + } + if additionalPingHandler == nil { + additionalPingHandler = func(_ string) error { return nil } + } + + conn.SetPingHandler(func(appData string) error { + if err := additionalPingHandler(appData); err != nil { + return err + } + + smc.handlePing(conn, appData) + + return nil + }) + + // We don't need to conn.SetCloseHandler because the default handler is effective enough that + // it sends back the CLOSE message to the server and let conn.ReadJSON() fail with CloseError. + // The CloseError must be handled normally in our receiveMessagesInto function. + //conn.SetCloseHandler(func(code int, text string) error { + // ... + // }) + + return info, conn, err +} + +// runResponseSender runs the handler that reads Socket Mode responses enqueued onto Client.socketModeResponses channel +// and sends them one by one over the WebSocket connection. +// Gorilla WebSocket is not goroutine safe hence this needs to be the single place you write to the WebSocket connection. +func (smc *Client) runResponseSender(ctx context.Context, conn *websocket.Conn) error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + // 3. listen for messages that need to be sent + case res := <-smc.socketModeResponses: + smc.Debugf("Sending Socket Mode response with envelope ID %q: %v", res.EnvelopeID, res) + + if err := unsafeWriteSocketModeResponse(conn, res); err != nil { + smc.sendEvent(ctx, newEvent(EventTypeErrorWriteFailed, &ErrorWriteFailed{ + Cause: err, + Response: res, + })) + } + + smc.Debugf("Finished sending Socket Mode response with envelope ID %q", res.EnvelopeID) + } + } +} + +// runRequestHandler is a blocking function that runs the Socket Mode request receiver. +// +// It reads WebSocket messages sent from Slack's Socket Mode WebSocket connection, +// parses them as Socket Mode requests, and processes them and optionally emit our own events into Client.Events channel. +func (smc *Client) runRequestHandler(ctx context.Context, websocket chan json.RawMessage) error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + case message, ok := <-websocket: + if !ok { + // The producer closed the channel because it encountered an error (or panic), + // we need only return. + return nil + } + + smc.Debugf("Received WebSocket message: %s", message) + + // listen for incoming messages that need to be parsed + evt, err := smc.parseEvent(message) + if err != nil { + smc.sendEvent(ctx, newEvent(EventTypeErrorBadMessage, &ErrorBadMessage{ + Cause: err, + Message: message, + })) + } else if evt != nil { + if evt.Type == EventTypeDisconnect { + // We treat the `disconnect` request from Slack as an error internally, + // so that we can tell the consumer of this function to reopen the connection on it. + return errorRequestedDisconnect{} + } + + smc.sendEvent(ctx, *evt) + } + } + } +} + +// runMessageReceiver monitors the Socket Mode opened WebSocket connection for any incoming +// messages. It pushes the raw events into the channel. +// The receiver runs until the context is closed. +func (smc *Client) runMessageReceiver(ctx context.Context, conn *websocket.Conn, sink chan json.RawMessage) error { + for { + if err := smc.receiveMessagesInto(ctx, conn, sink); err != nil { + return err + } + } +} + +// unsafeWriteSocketModeResponse sends a WebSocket message back to Slack. +// WARNING: Call to this function must be serialized! +// +// Here's why - Gorilla WebSocket's Writes functions are not concurrency-safe. +// That is, we must serialize all the writes to it with e.g. a goroutine or mutex. +// We intentionally chose to use goroutine, which makes it harder to propagate write errors to the caller, +// but is more computationally efficient. +// +// See the below for more information on this topic: +// https://stackoverflow.com/questions/43225340/how-to-ensure-concurrency-in-golang-gorilla-websocket-package +func unsafeWriteSocketModeResponse(conn *websocket.Conn, res *Response) error { + // set a write deadline on the connection + if err := conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { + return err + } + + // Remove write deadline regardless of WriteJSON succeeds or not + defer conn.SetWriteDeadline(time.Time{}) + + return conn.WriteJSON(res) +} + +func newEvent(tpe EventType, data interface{}, req ...*Request) Event { + evt := Event{Type: tpe, Data: data} + + if len(req) > 0 { + evt.Request = req[0] + } + + return evt +} + +// Ack acknowledges the Socket Mode request with the payload. +// +// This tells Slack that the we have received the request denoted by the envelope ID, +// by sending back the envelope ID over the WebSocket connection. +func (smc *Client) Ack(req Request, payload ...interface{}) { + var pld interface{} + if len(payload) > 0 { + pld = payload[0] + } + + smc.AckCtx(context.TODO(), req.EnvelopeID, pld) +} + +// AckCtx acknowledges the Socket Mode request envelope ID with the payload. +// +// This tells Slack that the we have received the request denoted by the request (envelope) ID, +// by sending back the ID over the WebSocket connection. +func (smc *Client) AckCtx(ctx context.Context, reqID string, payload interface{}) error { + return smc.SendCtx(ctx, Response{ + EnvelopeID: reqID, + Payload: payload, + }) +} + +// Send sends the Socket Mode response over a WebSocket connection. +// This is usually used for acknowledging requests, but if you need more control over Client.Ack(). +// It's normally recommended to use Client.Ack() instead of this. +func (smc *Client) Send(res Response) { + smc.SendCtx(context.TODO(), res) +} + +// SendCtx sends the Socket Mode response over a WebSocket connection. +// This is usually used for acknowledging requests, but if you need more control +// it's normally recommended to use Client.AckCtx() instead of this. +func (smc *Client) SendCtx(ctx context.Context, res Response) error { + if smc.debug { + js, err := json.Marshal(res) + + // Log the error so users of `Send` don't see it entirely disappear as that method + // does not return an error and used to panic on failure (with or without debug) + smc.Debugf("Scheduling Socket Mode response (error: %v) for envelope ID %s: %s", err, res.EnvelopeID, js) + if err != nil { + return err + } + } + + select { + case <-ctx.Done(): + return ctx.Err() + case smc.socketModeResponses <- &res: + } + + return nil +} + +// receiveMessagesInto attempts to receive an event from the WebSocket connection for Socket Mode. +// This will block until a frame is available from the WebSocket. +// If the read from the WebSocket results in a fatal error, this function will return non-nil. +func (smc *Client) receiveMessagesInto(ctx context.Context, conn *websocket.Conn, sink chan json.RawMessage) error { + smc.Debugf("Starting to receive message") + defer smc.Debugf("Finished to receive message") + + event := json.RawMessage{} + err := conn.ReadJSON(&event) + if err != nil { + // check if the connection was closed. + // This version of the gorilla/websocket package also does a type assertion + // on the error, rather than unwrapping it, so we'll do the unwrapping then pass + // the unwrapped error + var wsErr *websocket.CloseError + if errors.As(err, &wsErr) && websocket.IsUnexpectedCloseError(wsErr) { + return err + } + + if errors.Is(err, io.ErrUnexpectedEOF) { + // EOF's don't seem to signify a failed connection so instead we ignore + // them here and detect a failed connection upon attempting to send a + // 'PING' message + + // Unlike RTM, we don't ping from the our end as there seem to have no client ping. + // We just continue to the next loop so that we `smc.disconnected` should be received if + // this EOF error was actually due to disconnection. + + return nil + } + + // All other errors from ReadJSON come from NextReader, and should + // kill the read loop and force a reconnect. + // TODO: Unless it's a JSON unmarshal-type error in which case maybe reconnecting isn't needed... + smc.sendEvent(ctx, newEvent(EventTypeIncomingError, &slack.IncomingEventError{ + ErrorObj: err, + })) + + return err + } + + if smc.debug { + buf := &bytes.Buffer{} + d := json.NewEncoder(buf) + d.SetIndent("", " ") + if err := d.Encode(event); err != nil { + smc.Debugln("Failed encoding decoded json:", err) + } + reencoded := buf.String() + + smc.Debugln("Incoming WebSocket message:", reencoded) + } + + select { + case sink <- event: + case <-ctx.Done(): + smc.Debugln("cancelled while attempting to send raw event") + + return ctx.Err() + } + + return nil +} + +// parseEvent takes a raw JSON message received from the slack websocket +// and handles the encoded event. +// returns the our own event that wraps the socket mode request. +func (smc *Client) parseEvent(wsMsg json.RawMessage) (*Event, error) { + req := &Request{} + err := json.Unmarshal(wsMsg, req) + if err != nil { + return nil, fmt.Errorf("unmarshalling WebSocket message: %w", err) + } + + var evt Event + + // See below two links for all the available message types. + // - https://github.com/slackapi/node-slack-sdk/blob/c3f4d7109062a0356fb765d53794b7b5f6b3b5ae/packages/socket-mode/src/SocketModeClient.ts#L533 + // - https://api.slack.com/apis/connections/socket-implement + switch req.Type { + case RequestTypeHello: + evt = newEvent(EventTypeHello, nil, req) + case RequestTypeEventsAPI: + payloadEvent := req.Payload + + eventsAPIEvent, err := slackevents.ParseEvent(payloadEvent, slackevents.OptionNoVerifyToken()) + if err != nil { + return nil, fmt.Errorf("parsing Events API event: %w", err) + } + + evt = newEvent(EventTypeEventsAPI, eventsAPIEvent, req) + case RequestTypeDisconnect: + // See https://api.slack.com/apis/connections/socket-implement#disconnect + + evt = newEvent(EventTypeDisconnect, nil, req) + case RequestTypeSlashCommands: + // See https://api.slack.com/apis/connections/socket-implement#command + var cmd slack.SlashCommand + + if err := json.Unmarshal(req.Payload, &cmd); err != nil { + return nil, fmt.Errorf("parsing slash command: %w", err) + } + + evt = newEvent(EventTypeSlashCommand, cmd, req) + case RequestTypeInteractive: + // See belows: + // - https://api.slack.com/apis/connections/socket-implement#button + // - https://api.slack.com/apis/connections/socket-implement#home + // - https://api.slack.com/apis/connections/socket-implement#modal + // - https://api.slack.com/apis/connections/socket-implement#menu + + var callback slack.InteractionCallback + + if err := json.Unmarshal(req.Payload, &callback); err != nil { + return nil, fmt.Errorf("parsing interaction callback: %w", err) + } + + evt = newEvent(EventTypeInteractive, callback, req) + default: + return nil, fmt.Errorf("processing WebSocket message: encountered unsupported type %q", req.Type) + } + + return &evt, nil +} + +// handlePing handles an incoming 'PONG' message which should be in response to +// a previously sent 'PING' message. This is then used to compute the +// connection's latency. +func (smc *Client) handlePing(conn *websocket.Conn, event string) { + smc.Debugf("WebSocket ping message received: %s", event) + + // In WebSocket, we need to respond a PING from the server with a PONG with the same payload as the PING. + if err := conn.WriteControl(websocket.PongMessage, []byte(event), time.Now().Add(10*time.Second)); err != nil { + smc.Debugf("Failed writing WebSocket PONG message: %v", err) + } +} diff --git a/vendor/github.com/slack-go/slack/socketmode/socketmode.go b/vendor/github.com/slack-go/slack/socketmode/socketmode.go new file mode 100644 index 0000000000..6ca8f487c8 --- /dev/null +++ b/vendor/github.com/slack-go/slack/socketmode/socketmode.go @@ -0,0 +1,133 @@ +package socketmode + +import ( + "context" + "log" + "os" + "time" + + "github.com/gorilla/websocket" + + "github.com/slack-go/slack" +) + +// EventType is the type of events that are emitted by scoketmode.Client. +// You receive and handle those events from a socketmode.Client.Events channel. +// Those event types does not necessarily match 1:1 to those of Slack Events API events. +type EventType string + +const ( + // The following request types are the types of requests sent from Slack via Socket Mode WebSocket connection + // and handled internally by the socketmode.Client. + // The consumer of socketmode.Client will never see it. + + RequestTypeHello = "hello" + RequestTypeEventsAPI = "events_api" + RequestTypeDisconnect = "disconnect" + RequestTypeSlashCommands = "slash_commands" + RequestTypeInteractive = "interactive" + + // The following event types are for events emitted by socketmode.Client itself and + // does not originate from Slack. + EventTypeConnecting = EventType("connecting") + EventTypeInvalidAuth = EventType("invalid_auth") + EventTypeConnectionError = EventType("connection_error") + EventTypeConnected = EventType("connected") + EventTypeIncomingError = EventType("incoming_error") + EventTypeErrorWriteFailed = EventType("write_error") + EventTypeErrorBadMessage = EventType("error_bad_message") + + // + // The following event types are guaranteed to not change unless Slack changes + // + + EventTypeHello = EventType("hello") + EventTypeDisconnect = EventType("disconnect") + EventTypeEventsAPI = EventType("events_api") + EventTypeInteractive = EventType("interactive") + EventTypeSlashCommand = EventType("slash_commands") + + websocketDefaultTimeout = 10 * time.Second + defaultMaxPingInterval = 30 * time.Second +) + +// Open calls the "apps.connections.open" endpoint and returns the provided URL and the full Info block. +// +// To have a fully managed Websocket connection, use `New`, and call `Run()` on it. +func (smc *Client) Open() (info *slack.SocketModeConnection, websocketURL string, err error) { + ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout) + defer cancel() + + return smc.StartSocketModeContext(ctx) +} + +// OpenContext calls the "apps.connections.open" endpoint and returns the provided URL and the full Info block. +// +// To have a fully managed Websocket connection, use `New`, and call `Run()` on it. +func (smc *Client) OpenContext(ctx context.Context) (info *slack.SocketModeConnection, websocketURL string, err error) { + return smc.StartSocketModeContext(ctx) +} + +// Option options for the managed Client. +type Option func(client *Client) + +// OptionDialer takes a gorilla websocket Dialer and uses it as the +// Dialer when opening the websocket for the Socket Mode connection. +func OptionDialer(d *websocket.Dialer) Option { + return func(smc *Client) { + smc.dialer = d + } +} + +// OptionPingInterval determines how often we expect Slack to deliver WebSocket ping to us. +// If no ping is delivered to us within this interval after the last ping, we assumes the WebSocket connection +// is dead and needs to be reconnected. +func OptionPingInterval(d time.Duration) Option { + return func(smc *Client) { + smc.maxPingInterval = d + } +} + +// OptionDebug enable debugging for the client +func OptionDebug(b bool) func(*Client) { + return func(c *Client) { + c.debug = b + } +} + +// OptionLog set logging for client. +func OptionLog(l logger) func(*Client) { + return func(c *Client) { + c.log = internalLog{logger: l} + } +} + +// New returns a Socket Mode client which provides a fully managed connection to +// Slack's Websocket-based Socket Mode. +func New(api *slack.Client, options ...Option) *Client { + result := &Client{ + Client: *api, + Events: make(chan Event, 50), + socketModeResponses: make(chan *Response, 20), + maxPingInterval: defaultMaxPingInterval, + log: log.New(os.Stderr, "slack-go/slack/socketmode", log.LstdFlags|log.Lshortfile), + } + + for _, opt := range options { + opt(result) + } + + return result +} + +// sendEvent safely sends an event into the Clients Events channel +// and blocks until buffer space is had, or the context is canceled. +// This prevents deadlocking in the event that Events buffer is full, +// other goroutines are waiting, and/or timing allows receivers to exit +// before all senders are finished. +func (smc *Client) sendEvent(ctx context.Context, event Event) { + select { + case smc.Events <- event: + case <-ctx.Done(): + } +} diff --git a/vendor/github.com/slack-go/slack/socketmode/socketmode_handler.go b/vendor/github.com/slack-go/slack/socketmode/socketmode_handler.go new file mode 100644 index 0000000000..0cfe7555d8 --- /dev/null +++ b/vendor/github.com/slack-go/slack/socketmode/socketmode_handler.go @@ -0,0 +1,260 @@ +package socketmode + +import ( + "context" + "github.com/slack-go/slack" + "github.com/slack-go/slack/slackevents" +) + +type SocketmodeHandler struct { + Client *Client + + //lvl 1 - the most generic type of event + EventMap map[EventType][]SocketmodeHandlerFunc + //lvl 2 - Manage event by inner type + InteractionEventMap map[slack.InteractionType][]SocketmodeHandlerFunc + EventApiMap map[slackevents.EventsAPIType][]SocketmodeHandlerFunc + //lvl 3 - the most userfriendly way of managing event + InteractionBlockActionEventMap map[string]SocketmodeHandlerFunc + SlashCommandMap map[string]SocketmodeHandlerFunc + + Default SocketmodeHandlerFunc +} + +// Handler have access to the event and socketmode client +type SocketmodeHandlerFunc func(*Event, *Client) + +// Middleware accept SocketmodeHandlerFunc, and return SocketmodeHandlerFunc +type SocketmodeMiddlewareFunc func(SocketmodeHandlerFunc) SocketmodeHandlerFunc + +// Initialization constructor for SocketmodeHandler +func NewSocketmodeHandler(client *Client) *SocketmodeHandler { + eventMap := make(map[EventType][]SocketmodeHandlerFunc) + interactionEventMap := make(map[slack.InteractionType][]SocketmodeHandlerFunc) + eventApiMap := make(map[slackevents.EventsAPIType][]SocketmodeHandlerFunc) + + interactionBlockActionEventMap := make(map[string]SocketmodeHandlerFunc) + slackCommandMap := make(map[string]SocketmodeHandlerFunc) + + return &SocketmodeHandler{ + Client: client, + EventMap: eventMap, + EventApiMap: eventApiMap, + InteractionEventMap: interactionEventMap, + InteractionBlockActionEventMap: interactionBlockActionEventMap, + SlashCommandMap: slackCommandMap, + Default: func(e *Event, c *Client) { + c.log.Printf("Unexpected event type received: %v\n", e.Type) + }, + } +} + +// Register a middleware or handler for an Event from socketmode +// This most general entrypoint +func (r *SocketmodeHandler) Handle(et EventType, f SocketmodeHandlerFunc) { + r.EventMap[et] = append(r.EventMap[et], f) +} + +// Register a middleware or handler for an Interaction +// There is several types of interactions, decated functions lets you better handle them +// See +// * HandleInteractionBlockAction +// * (Not Implemented) HandleShortcut +// * (Not Implemented) HandleView +func (r *SocketmodeHandler) HandleInteraction(et slack.InteractionType, f SocketmodeHandlerFunc) { + r.InteractionEventMap[et] = append(r.InteractionEventMap[et], f) +} + +// Register a middleware or handler for a Block Action referenced by its ActionID +func (r *SocketmodeHandler) HandleInteractionBlockAction(actionID string, f SocketmodeHandlerFunc) { + if actionID == "" { + panic("invalid command cannot be empty") + } + if f == nil { + panic("invalid handler cannot be nil") + } + if _, exist := r.InteractionBlockActionEventMap[actionID]; exist { + panic("multiple registrations for actionID" + actionID) + } + r.InteractionBlockActionEventMap[actionID] = f +} + +// Register a middleware or handler for an Event (from slackevents) +func (r *SocketmodeHandler) HandleEvents(et slackevents.EventsAPIType, f SocketmodeHandlerFunc) { + r.EventApiMap[et] = append(r.EventApiMap[et], f) +} + +// Register a middleware or handler for a Slash Command +func (r *SocketmodeHandler) HandleSlashCommand(command string, f SocketmodeHandlerFunc) { + if command == "" { + panic("invalid command cannot be empty") + } + if f == nil { + panic("invalid handler cannot be nil") + } + if _, exist := r.SlashCommandMap[command]; exist { + panic("multiple registrations for command" + command) + } + r.SlashCommandMap[command] = f +} + +// Register a middleware or handler to use as a last resort +func (r *SocketmodeHandler) HandleDefault(f SocketmodeHandlerFunc) { + r.Default = f +} + +// RunSlackEventLoop receives the event via the socket +func (r *SocketmodeHandler) RunEventLoop() error { + + go r.runEventLoop(context.Background()) + + return r.Client.Run() +} + +func (r *SocketmodeHandler) RunEventLoopContext(ctx context.Context) error { + go r.runEventLoop(ctx) + + return r.Client.RunContext(ctx) +} + +// Call the dispatcher for each incomming event +func (r *SocketmodeHandler) runEventLoop(ctx context.Context) { + for { + select { + case evt, ok := <-r.Client.Events: + if !ok { + return + } + + r.dispatcher(evt) + + case <-ctx.Done(): + return + } + } +} + +// Dispatch events to the specialized dispatcher +func (r *SocketmodeHandler) dispatcher(evt Event) { + var ishandled bool + + // Some eventType can be further decomposed + switch evt.Type { + case EventTypeInteractive: + ishandled = r.interactionDispatcher(&evt) + case EventTypeEventsAPI: + ishandled = r.eventAPIDispatcher(&evt) + case EventTypeSlashCommand: + ishandled = r.slashCommandDispatcher(&evt) + default: + ishandled = r.socketmodeDispatcher(&evt) + } + + if !ishandled { + go r.Default(&evt, r.Client) + } +} + +// Dispatch socketmode events to the registered middleware +func (r *SocketmodeHandler) socketmodeDispatcher(evt *Event) bool { + if handlers, ok := r.EventMap[evt.Type]; ok { + // If we registered an event + for _, f := range handlers { + go f(evt, r.Client) + } + + return true + } + + return false +} + +// Dispatch interactions to the registered middleware +func (r *SocketmodeHandler) interactionDispatcher(evt *Event) bool { + var ishandled bool = false + + interaction, ok := evt.Data.(slack.InteractionCallback) + if !ok { + r.Client.log.Printf("Ignored %+v\n", evt) + return false + } + + // Level 1 - socketmode EventType + ishandled = r.socketmodeDispatcher(evt) + + // Level 2 - interaction EventType + if handlers, ok := r.InteractionEventMap[interaction.Type]; ok { + // If we registered an event + for _, f := range handlers { + go f(evt, r.Client) + } + + ishandled = true + } + + // Level 3 - interaction with actionID + blockActions := interaction.ActionCallback.BlockActions + // outmoded approach won`t be implemented + // attachments_actions := interaction.ActionCallback.AttachmentActions + + for _, action := range blockActions { + if handler, ok := r.InteractionBlockActionEventMap[action.ActionID]; ok { + + go handler(evt, r.Client) + + ishandled = true + } + } + return ishandled +} + +// Dispatch eventAPI events to the registered middleware +func (r *SocketmodeHandler) eventAPIDispatcher(evt *Event) bool { + var ishandled bool = false + eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) + if !ok { + r.Client.log.Printf("Ignored %+v\n", evt) + return false + } + + innerEventType := slackevents.EventsAPIType(eventsAPIEvent.InnerEvent.Type) + + // Level 1 - socketmode EventType + ishandled = r.socketmodeDispatcher(evt) + + // Level 2 - EventAPI EventType + if handlers, ok := r.EventApiMap[innerEventType]; ok { + // If we registered an event + for _, f := range handlers { + go f(evt, r.Client) + } + + ishandled = true + } + + return ishandled +} + +// Dispatch SlashCommands events to the registered middleware +func (r *SocketmodeHandler) slashCommandDispatcher(evt *Event) bool { + var ishandled bool = false + slashCommandEvent, ok := evt.Data.(slack.SlashCommand) + if !ok { + r.Client.log.Printf("Ignored %+v\n", evt) + return false + } + + // Level 1 - socketmode EventType + ishandled = r.socketmodeDispatcher(evt) + + // Level 2 - SlackCommand by name + if handler, ok := r.SlashCommandMap[slashCommandEvent.Command]; ok { + + go handler(evt, r.Client) + + ishandled = true + } + + return ishandled + +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 93aad2acd2..2371c1c4c9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -381,7 +381,9 @@ github.com/slack-go/slack github.com/slack-go/slack/internal/backoff github.com/slack-go/slack/internal/errorsx github.com/slack-go/slack/internal/timex +github.com/slack-go/slack/slackevents github.com/slack-go/slack/slackutilsx +github.com/slack-go/slack/socketmode # github.com/sourcegraph/conc v0.3.0 ## explicit; go 1.19 github.com/sourcegraph/conc