Skip to content

Commit

Permalink
Merge pull request #904 from xNok/socketmode-handler
Browse files Browse the repository at this point in the history
Socketmode Middleware Design Pattern
  • Loading branch information
kanata2 committed Jun 2, 2022
2 parents 34c7fd3 + 2a3c452 commit 4bbfcfb
Show file tree
Hide file tree
Showing 10 changed files with 1,020 additions and 32 deletions.
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -86,7 +86,13 @@ See https://github.com/slack-go/slack/blob/master/examples/websocket/websocket.g

See https://github.com/slack-go/slack/blob/master/examples/eventsapi/events.go

## Socketmode Event Handler (Experimental)

When using socket mode, dealing with an event can be pretty lengthy as it requires you to route the event to the right place.

Instead, you can use `SocketmodeHandler` much like you use an HTTP handler to register which event you would like to listen to and what callback function will process that event when it occurs.

See [./examples/socketmode_handler/socketmode_handler.go](./examples/socketmode_handler/socketmode_handler.go)
## Contributing

You are more than welcome to contribute to this project. Fork and
Expand Down
205 changes: 205 additions & 0 deletions examples/socketmode_handler/socketmode_handler.go
@@ -0,0 +1,205 @@
package main

import (
"fmt"
"log"
"os"
"strings"

"github.com/slack-go/slack/slackevents"
"github.com/slack-go/slack/socketmode"

"github.com/slack-go/slack"
)

func main() {
appToken := os.Getenv("SLACK_APP_TOKEN")
if appToken == "" {

}

if !strings.HasPrefix(appToken, "xapp-") {
panic("SLACK_APP_TOKEN must have the prefix \"xapp-\".")
}

botToken := os.Getenv("SLACK_BOT_TOKEN")
if botToken == "" {
panic("SLACK_BOT_TOKEN must be set.\n")
}

if !strings.HasPrefix(botToken, "xoxb-") {
panic("SLACK_BOT_TOKEN must have the prefix \"xoxb-\".")
}

api := slack.New(
botToken,
slack.OptionDebug(true),
slack.OptionLog(log.New(os.Stdout, "api: ", log.Lshortfile|log.LstdFlags)),
slack.OptionAppLevelToken(appToken),
)

client := socketmode.New(
api,
socketmode.OptionDebug(true),
socketmode.OptionLog(log.New(os.Stdout, "socketmode: ", log.Lshortfile|log.LstdFlags)),
)

socketmodeHandler := socketmode.NewSocketmodeHandler(client)

socketmodeHandler.Handle(socketmode.EventTypeConnecting, middlewareConnecting)
socketmodeHandler.Handle(socketmode.EventTypeConnectionError, middlewareConnectionError)
socketmodeHandler.Handle(socketmode.EventTypeConnected, middlewareConnected)

//\\ EventTypeEventsAPI //\\
// Handle all EventsAPI
socketmodeHandler.Handle(socketmode.EventTypeEventsAPI, middlewareEventsAPI)

// Handle a specific event from EventsAPI
socketmodeHandler.HandleEvents(slackevents.AppMention, middlewareAppMentionEvent)

//\\ EventTypeInteractive //\\
// Handle all Interactive Events
socketmodeHandler.Handle(socketmode.EventTypeInteractive, middlewareInteractive)

// Handle a specific Interaction
socketmodeHandler.HandleInteraction(slack.InteractionTypeBlockActions, middlewareInteractionTypeBlockActions)

// Handle all SlashCommand
socketmodeHandler.Handle(socketmode.EventTypeSlashCommand, middlewareSlashCommand)
socketmodeHandler.HandleSlashCommand("/rocket", middlewareSlashCommand)

// socketmodeHandler.HandleDefault(middlewareDefault)

socketmodeHandler.RunEventLoop()
}

func middlewareConnecting(evt *socketmode.Event, client *socketmode.Client) {
fmt.Println("Connecting to Slack with Socket Mode...")
}

func middlewareConnectionError(evt *socketmode.Event, client *socketmode.Client) {
fmt.Println("Connection failed. Retrying later...")
}

func middlewareConnected(evt *socketmode.Event, client *socketmode.Client) {
fmt.Println("Connected to Slack with Socket Mode.")
}

func middlewareEventsAPI(evt *socketmode.Event, client *socketmode.Client) {
fmt.Println("middlewareEventsAPI")
eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
if !ok {
fmt.Printf("Ignored %+v\n", evt)
return
}

fmt.Printf("Event received: %+v\n", eventsAPIEvent)

client.Ack(*evt.Request)

switch eventsAPIEvent.Type {
case slackevents.CallbackEvent:
innerEvent := eventsAPIEvent.InnerEvent
switch ev := innerEvent.Data.(type) {
case *slackevents.AppMentionEvent:
fmt.Printf("We have been mentionned in %v", ev.Channel)
_, _, err := client.Client.PostMessage(ev.Channel, slack.MsgOptionText("Yes, hello.", false))
if err != nil {
fmt.Printf("failed posting message: %v", err)
}
case *slackevents.MemberJoinedChannelEvent:
fmt.Printf("user %q joined to channel %q", ev.User, ev.Channel)
}
default:
client.Debugf("unsupported Events API event received")
}
}

func middlewareAppMentionEvent(evt *socketmode.Event, client *socketmode.Client) {

eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
if !ok {
fmt.Printf("Ignored %+v\n", evt)
return
}

client.Ack(*evt.Request)

ev, ok := eventsAPIEvent.InnerEvent.Data.(*slackevents.AppMentionEvent)
if !ok {
fmt.Printf("Ignored %+v\n", ev)
return
}

fmt.Printf("We have been mentionned in %v\n", ev.Channel)
_, _, err := client.Client.PostMessage(ev.Channel, slack.MsgOptionText("Yes, hello.", false))
if err != nil {
fmt.Printf("failed posting message: %v", err)
}
}

func middlewareInteractive(evt *socketmode.Event, client *socketmode.Client) {
callback, ok := evt.Data.(slack.InteractionCallback)
if !ok {
fmt.Printf("Ignored %+v\n", evt)
return
}

fmt.Printf("Interaction received: %+v\n", callback)

var payload interface{}

switch callback.Type {
case slack.InteractionTypeBlockActions:
// See https://api.slack.com/apis/connections/socket-implement#button
client.Debugf("button clicked!")
case slack.InteractionTypeShortcut:
case slack.InteractionTypeViewSubmission:
// See https://api.slack.com/apis/connections/socket-implement#modal
case slack.InteractionTypeDialogSubmission:
default:

}

client.Ack(*evt.Request, payload)
}

func middlewareInteractionTypeBlockActions(evt *socketmode.Event, client *socketmode.Client) {
client.Debugf("button clicked!")
}

func middlewareSlashCommand(evt *socketmode.Event, client *socketmode.Client) {
cmd, ok := evt.Data.(slack.SlashCommand)
if !ok {
fmt.Printf("Ignored %+v\n", evt)
return
}

client.Debugf("Slash command received: %+v", cmd)

payload := map[string]interface{}{
"blocks": []slack.Block{
slack.NewSectionBlock(
&slack.TextBlockObject{
Type: slack.MarkdownType,
Text: "foo",
},
nil,
slack.NewAccessory(
slack.NewButtonBlockElement(
"",
"somevalue",
&slack.TextBlockObject{
Type: slack.PlainTextType,
Text: "bar",
},
),
),
),
}}
client.Ack(*evt.Request, payload)
}

func middlewareDefault(evt *socketmode.Event, client *socketmode.Client) {
// fmt.Fprintf(os.Stderr, "Unexpected event type received: %s\n", evt.Type)
}
60 changes: 31 additions & 29 deletions slackevents/inner_events.go
Expand Up @@ -429,71 +429,73 @@ func (e MessageEvent) IsEdited() bool {
e.Message.Edited != nil
}

type EventsAPIType string

const (
// AppMention is an Events API subscribable event
AppMention = "app_mention"
AppMention = EventsAPIType("app_mention")
// AppHomeOpened Your Slack app home was opened
AppHomeOpened = "app_home_opened"
AppHomeOpened = EventsAPIType("app_home_opened")
// AppUninstalled Your Slack app was uninstalled.
AppUninstalled = "app_uninstalled"
AppUninstalled = EventsAPIType("app_uninstalled")
// ChannelCreated is sent when a new channel is created.
ChannelCreated = "channel_created"
ChannelCreated = EventsAPIType("channel_created")
// ChannelDeleted is sent when a channel is deleted.
ChannelDeleted = "channel_deleted"
ChannelDeleted = EventsAPIType("channel_deleted")
// ChannelArchive is sent when a channel is archived.
ChannelArchive = "channel_archive"
ChannelArchive = EventsAPIType("channel_archive")
// ChannelUnarchive is sent when a channel is unarchived.
ChannelUnarchive = "channel_unarchive"
ChannelUnarchive = EventsAPIType("channel_unarchive")
// ChannelLeft is sent when a channel is left.
ChannelLeft = "channel_left"
ChannelLeft = EventsAPIType("channel_left")
// ChannelRename is sent when a channel is rename.
ChannelRename = "channel_rename"
ChannelRename = EventsAPIType("channel_rename")
// ChannelIDChanged is sent when a channel identifier is changed.
ChannelIDChanged = "channel_id_changed"
// GroupDeleted is sent when a group is deleted.
GroupDeleted = "group_deleted"
GroupDeleted = EventsAPIType("group_deleted")
// GroupArchive is sent when a group is archived.
GroupArchive = "group_archive"
GroupArchive = EventsAPIType("group_archive")
// GroupUnarchive is sent when a group is unarchived.
GroupUnarchive = "group_unarchive"
GroupUnarchive = EventsAPIType("group_unarchive")
// GroupLeft is sent when a group is left.
GroupLeft = "group_left"
GroupLeft = EventsAPIType("group_left")
// GroupRename is sent when a group is renamed.
GroupRename = "group_rename"
GroupRename = EventsAPIType("group_rename")
// GridMigrationFinished An enterprise grid migration has finished on this workspace.
GridMigrationFinished = "grid_migration_finished"
GridMigrationFinished = EventsAPIType("grid_migration_finished")
// GridMigrationStarted An enterprise grid migration has started on this workspace.
GridMigrationStarted = "grid_migration_started"
GridMigrationStarted = EventsAPIType("grid_migration_started")
// LinkShared A message was posted containing one or more links relevant to your application
LinkShared = "link_shared"
LinkShared = EventsAPIType("link_shared")
// Message A message was posted to a channel, private channel (group), im, or mim
Message = "message"
Message = EventsAPIType("message")
// Member Joined Channel
MemberJoinedChannel = "member_joined_channel"
MemberJoinedChannel = EventsAPIType("member_joined_channel")
// Member Left Channel
MemberLeftChannel = "member_left_channel"
MemberLeftChannel = EventsAPIType("member_left_channel")
// PinAdded An item was pinned to a channel
PinAdded = "pin_added"
PinAdded = EventsAPIType("pin_added")
// PinRemoved An item was unpinned from a channel
PinRemoved = "pin_removed"
PinRemoved = EventsAPIType("pin_removed")
// ReactionAdded An reaction was added to a message
ReactionAdded = "reaction_added"
ReactionAdded = EventsAPIType("reaction_added")
// ReactionRemoved An reaction was removed from a message
ReactionRemoved = "reaction_removed"
ReactionRemoved = EventsAPIType("reaction_removed")
// TeamJoin A new user joined the workspace
TeamJoin = "team_join"
TeamJoin = EventsAPIType("team_join")
// TokensRevoked APP's API tokes are revoked
TokensRevoked = "tokens_revoked"
TokensRevoked = EventsAPIType("tokens_revoked")
// EmojiChanged A custom emoji has been added or changed
EmojiChanged = "emoji_changed"
EmojiChanged = EventsAPIType("emoji_changed")
// WorkflowStepExecute Happens, if a workflow step of your app is invoked
WorkflowStepExecute = "workflow_step_execute"
WorkflowStepExecute = EventsAPIType("workflow_step_execute")
)

// 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[string]interface{}{
var EventsAPIInnerEventMapping = map[EventsAPIType]interface{}{
AppMention: AppMentionEvent{},
AppHomeOpened: AppHomeOpenedEvent{},
AppUninstalled: AppUninstalledEvent{},
Expand Down
2 changes: 1 addition & 1 deletion slackevents/parsers.go
Expand Up @@ -19,7 +19,7 @@ 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[t]
v, exists := EventsAPIInnerEventMapping[EventsAPIType(t)]
if exists {
return v, exists
}
Expand Down
6 changes: 6 additions & 0 deletions slacktest/data.go
Expand Up @@ -35,6 +35,12 @@ var okWebResponse = slack.SlackResponse{
Ok: true,
}

var defaultOkJSON = fmt.Sprintf(`
{
"ok": true
}
`)

var defaultChannelsListJSON = fmt.Sprintf(`
{
"ok": true,
Expand Down
10 changes: 10 additions & 0 deletions slacktest/handlers.go
Expand Up @@ -108,6 +108,16 @@ func inviteConversationHandler(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(inviteConversationJSON))
}

// handle groups.list
func listGroupsHandler(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(defaultGroupsListJSON))
}

// handle reaction.Add
func reactionAddHandler(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(defaultOkJSON))
}

// handle chat.postMessage
func (sts *Server) postMessageHandler(w http.ResponseWriter, r *http.Request) {
serverAddr := r.Context().Value(ServerBotHubNameContextKey).(string)
Expand Down
1 change: 1 addition & 0 deletions slacktest/server.go
Expand Up @@ -59,6 +59,7 @@ func NewTestServer(custom ...binder) *Server {
s.Handle("/users.lookupByEmail", usersInfoHandler)
s.Handle("/bots.info", botsInfoHandler)
s.Handle("/auth.test", authTestHandler)
s.Handle("/reactions.add", reactionAddHandler)

httpserver := httptest.NewUnstartedServer(s.mux)
addr := httpserver.Listener.Addr().String()
Expand Down

0 comments on commit 4bbfcfb

Please sign in to comment.