-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #885 from mumoshu/socket-mode
Socket Mode support
- Loading branch information
Showing
16 changed files
with
1,411 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"os" | ||
"strings" | ||
|
||
"github.com/slack-go/slack/socketmode" | ||
|
||
"github.com/slack-go/slack" | ||
"github.com/slack-go/slack/slackevents" | ||
) | ||
|
||
func main() { | ||
appToken := os.Getenv("SLACK_APP_TOKEN") | ||
if appToken == "" { | ||
|
||
} | ||
|
||
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)), | ||
) | ||
|
||
go func() { | ||
for evt := range client.Events { | ||
switch evt.Type { | ||
case socketmode.EventTypeConnecting: | ||
fmt.Println("Connecting to Slack with Socket Mode...") | ||
case socketmode.EventTypeConnectionError: | ||
fmt.Println("Connection failed. Retrying later...") | ||
case socketmode.EventTypeConnected: | ||
fmt.Println("Connected to Slack with Socket Mode.") | ||
case socketmode.EventTypeEventsAPI: | ||
eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) | ||
if !ok { | ||
fmt.Printf("Ignored %+v\n", evt) | ||
|
||
continue | ||
} | ||
|
||
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: | ||
_, _, err := api.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") | ||
} | ||
case socketmode.EventTypeInteractive: | ||
callback, ok := evt.Data.(slack.InteractionCallback) | ||
if !ok { | ||
fmt.Printf("Ignored %+v\n", evt) | ||
|
||
continue | ||
} | ||
|
||
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) | ||
case socketmode.EventTypeSlashCommand: | ||
cmd, ok := evt.Data.(slack.SlashCommand) | ||
if !ok { | ||
fmt.Printf("Ignored %+v\n", evt) | ||
|
||
continue | ||
} | ||
|
||
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) | ||
default: | ||
fmt.Fprintf(os.Stderr, "Unexpected event type received: %s\n", evt.Type) | ||
} | ||
} | ||
}() | ||
|
||
client.Run() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package misc | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
) | ||
|
||
// StatusCodeError represents an http response error. | ||
// type httpStatusCode interface { HTTPStatusCode() int } to handle it. | ||
type StatusCodeError struct { | ||
Code int | ||
Status string | ||
} | ||
|
||
func (t StatusCodeError) Error() string { | ||
return fmt.Sprintf("slack server error: %s", t.Status) | ||
} | ||
|
||
func (t StatusCodeError) HTTPStatusCode() int { | ||
return t.Code | ||
} | ||
|
||
func (t StatusCodeError) Retryable() bool { | ||
if t.Code >= 500 || t.Code == http.StatusTooManyRequests { | ||
return true | ||
} | ||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package slack | ||
|
||
import ( | ||
"context" | ||
) | ||
|
||
// SocketModeConnection contains various details about the SocketMode connection. | ||
// It is returned by an "apps.connections.open" API call. | ||
type SocketModeConnection struct { | ||
URL string `json:"url,omitempty"` | ||
Data map[string]interface{} `json:"-"` | ||
} | ||
|
||
type openResponseFull struct { | ||
SlackResponse | ||
SocketModeConnection | ||
} | ||
|
||
// StartSocketModeContext calls the "apps.connections.open" endpoint and returns the provided URL and the full Info block with a custom context. | ||
// | ||
// To have a fully managed Socket Mode connection, use `socketmode.New()`, and call `Run()` on it. | ||
func (api *Client) StartSocketModeContext(ctx context.Context) (info *SocketModeConnection, websocketURL string, err error) { | ||
response := &openResponseFull{} | ||
err = postJSON(ctx, api.httpclient, api.endpoint+"apps.connections.open", api.appLevelToken, nil, response, api) | ||
if err != nil { | ||
return nil, "", err | ||
} | ||
|
||
if response.Err() == nil { | ||
api.Debugln("Using URL:", response.SocketModeConnection.URL) | ||
} | ||
|
||
return &response.SocketModeConnection, response.SocketModeConnection.URL, response.Err() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
apiClient 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 | ||
} |
Oops, something went wrong.