Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Socketmode Middleware Design Pattern #904

Merged
merged 38 commits into from Jun 2, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
54bd12b
feat(slacktest): add handler for reaction.Add
xNok Jan 23, 2021
3178aab
feat: basic stucture
xNok Feb 21, 2021
65335cf
feat: socketmode Event Handling
xNok Feb 21, 2021
1dbd284
feat: handle all type of EventTypeInteractive
xNok Feb 22, 2021
6d8be03
feat: handler EventAPI inner events
xNok Feb 23, 2021
03e47e9
test: write unit tests
xNok Feb 24, 2021
2c8000f
feat: added type for SocketmodeMiddlewareFunc
xNok Feb 26, 2021
357e374
fix: typos
xNok Feb 27, 2021
e976c8a
fix: typos
xNok Feb 28, 2021
b1318d9
feat: Handle interaction block action by action ID
xNok Mar 8, 2021
28ed51b
fix: tests for socketmode
xNok Mar 9, 2021
5ceeab8
Merge branch 'master' into socketmode-handler
xNok Apr 15, 2021
2710f4e
Merge branch 'master' into socketmode-handler
xNok Apr 21, 2021
16e7557
fix: linting issue
xNok Apr 21, 2021
6410dfe
feat: dedicated handler for Slash commands
xNok May 8, 2021
e8ecf5e
fix: exemple
xNok May 8, 2021
9bc8016
fix: initialization controller
xNok May 8, 2021
9163026
fix: typo in example file
xNok Jun 2, 2021
dd086ec
refact: use client logger
xNok Jun 6, 2021
02281d1
refact: panic instead of os.Exit(1)
xNok Jun 6, 2021
cf9dd14
refact: rename HandleEventsAPI to HandleEvents
xNok Jun 6, 2021
fb69306
fix: rename action_id to actionID
xNok Jun 6, 2021
430c14a
refact: rename EventAPIType to EventsAPIType
xNok Jun 6, 2021
670527f
refact: update godoc
xNok Jun 8, 2021
296bd7e
refact: change the type of socketmode.apiClient from a non-pointer to…
xNok Jun 8, 2021
9bd9782
refact: RunEventLoop return errors that occured
xNok Jun 8, 2021
20e09d5
refact: rename file to socketmode_handler
xNok Jun 8, 2021
01e7385
refact: use socketmode client's logger.
xNok Jun 8, 2021
c965498
refact: rename block_actions to blockActions
xNok Jun 8, 2021
0273c6d
fix: side effect
xNok Jun 8, 2021
7a1949e
refact: use client logger
xNok Jun 8, 2021
38427ff
refact: must register unique action_id or slash commands
xNok Jun 8, 2021
c062aee
fix: typo
xNok Jul 11, 2021
6609fd5
Merge branch 'master' into socketmode-handler
xNok Nov 1, 2021
2135a67
fix: compatibility with access to Slack client in socketmode
xNok Nov 1, 2021
d06a99b
doc: socketmode handler
xNok Nov 8, 2021
b11c9d1
fix: trailing whitespace
xNok Nov 9, 2021
2a3c452
Merge branch 'master' into socketmode-handler
kanata2 Apr 24, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
206 changes: 206 additions & 0 deletions examples/sockermode_handler/socketmode_handler.go
@@ -0,0 +1,206 @@
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 == "" {

kanata2 marked this conversation as resolved.
Show resolved Hide resolved
}

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

botToken := os.Getenv("SLACK_BOT_TOKEN")
if botToken == "" {
fmt.Fprintf(os.Stderr, "SLACK_BOT_TOKEN must be set.\n")
os.Exit(1)
}

if !strings.HasPrefix(botToken, "xoxb-") {
fmt.Fprintf(os.Stderr, "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.NewsSocketmodeHandler(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.HandleEventsAPI(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.GetApiClient().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.GetApiClient().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)
}
36 changes: 19 additions & 17 deletions slackevents/inner_events.go
Expand Up @@ -319,45 +319,47 @@ func (e MessageEvent) IsEdited() bool {
e.Message.Edited != nil
}

type EventAPIType string
kanata2 marked this conversation as resolved.
Show resolved Hide resolved

const (
// AppMention is an Events API subscribable event
AppMention = "app_mention"
AppMention = EventAPIType("app_mention")
// AppHomeOpened Your Slack app home was opened
AppHomeOpened = "app_home_opened"
AppHomeOpened = EventAPIType("app_home_opened")
// AppUninstalled Your Slack app was uninstalled.
AppUninstalled = "app_uninstalled"
AppUninstalled = EventAPIType("app_uninstalled")
// ChannelCreated is sent when a new channel is created.
ChannelCreated = "channel_created"
ChannelCreated = EventAPIType("channel_created")
// GridMigrationFinished An enterprise grid migration has finished on this workspace.
GridMigrationFinished = "grid_migration_finished"
GridMigrationFinished = EventAPIType("grid_migration_finished")
// GridMigrationStarted An enterprise grid migration has started on this workspace.
GridMigrationStarted = "grid_migration_started"
GridMigrationStarted = EventAPIType("grid_migration_started")
// LinkShared A message was posted containing one or more links relevant to your application
LinkShared = "link_shared"
LinkShared = EventAPIType("link_shared")
// Message A message was posted to a channel, private channel (group), im, or mim
Message = "message"
Message = EventAPIType("message")
// Member Joined Channel
MemberJoinedChannel = "member_joined_channel"
MemberJoinedChannel = EventAPIType("member_joined_channel")
// Member Left Channel
MemberLeftChannel = "member_left_channel"
MemberLeftChannel = EventAPIType("member_left_channel")
// PinAdded An item was pinned to a channel
PinAdded = "pin_added"
PinAdded = EventAPIType("pin_added")
// PinRemoved An item was unpinned from a channel
PinRemoved = "pin_removed"
PinRemoved = EventAPIType("pin_removed")
// ReactionAdded An reaction was added to a message
ReactionAdded = "reaction_added"
ReactionAdded = EventAPIType("reaction_added")
// ReactionRemoved An reaction was removed from a message
ReactionRemoved = "reaction_removed"
ReactionRemoved = EventAPIType("reaction_removed")
// TokensRevoked APP's API tokes are revoked
TokensRevoked = "tokens_revoked"
TokensRevoked = EventAPIType("tokens_revoked")
// EmojiChanged A custom emoji has been added or changed
EmojiChanged = "emoji_changed"
EmojiChanged = EventAPIType("emoji_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[string]interface{}{
var EventsAPIInnerEventMapping = map[EventAPIType]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[EventAPIType(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 @@ -107,6 +107,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
5 changes: 5 additions & 0 deletions socketmode/client.go
Expand Up @@ -61,3 +61,8 @@ type Client struct {
debug bool
log ilogger
}

// return a pointer to the slack API client
func (c Client) GetApiClient() *slack.Client {
return &c.apiClient
kanata2 marked this conversation as resolved.
Show resolved Hide resolved
}