Skip to content

Commit

Permalink
Merge pull request #885 from mumoshu/socket-mode
Browse files Browse the repository at this point in the history
Socket Mode support
  • Loading branch information
kanata2 authored Jan 19, 2021
2 parents 4df030e + 0fbd39a commit 483202e
Show file tree
Hide file tree
Showing 16 changed files with 1,411 additions and 34 deletions.
149 changes: 149 additions & 0 deletions examples/socketmode/socketmode.go
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()
}
13 changes: 9 additions & 4 deletions backoff.go → internal/backoff/backoff.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package slack
package backoff

import (
"math/rand"
Expand All @@ -11,7 +11,7 @@ import (
// call to Duration() it is multiplied by Factor. It is capped at
// Max. It returns to Min on every call to Reset(). Used in
// conjunction with the time package.
type backoff struct {
type Backoff struct {
attempts int
// Initial value to scale out
Initial time.Duration
Expand All @@ -23,7 +23,7 @@ type backoff struct {

// Returns the current value of the counter and then multiplies it
// Factor
func (b *backoff) Duration() (dur time.Duration) {
func (b *Backoff) Duration() (dur time.Duration) {
// Zero-values are nonsensical, so we use
// them to apply defaults
if b.Max == 0 {
Expand Down Expand Up @@ -52,6 +52,11 @@ func (b *backoff) Duration() (dur time.Duration) {
}

//Resets the current value of the counter back to Min
func (b *backoff) Reset() {
func (b *Backoff) Reset() {
b.attempts = 0
}

// Attempts returns the number of attempts that we had done so far
func (b *Backoff) Attempts() int {
return b.attempts
}
28 changes: 28 additions & 0 deletions internal/misc/misc.go
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
}
26 changes: 3 additions & 23 deletions misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"strconv"
"strings"
"time"

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

// SlackResponse handles parsing out errors from the web api.
Expand All @@ -42,28 +44,6 @@ func (t SlackResponse) Err() error {
return errors.New(t.Error)
}

// 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
}

// RateLimitedError represents the rate limit respond from slack
type RateLimitedError struct {
RetryAfter time.Duration
Expand Down Expand Up @@ -312,7 +292,7 @@ func checkStatusCode(resp *http.Response, d Debug) error {
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != http.StatusOK {
logResponse(resp, d)
return statusCodeError{Code: resp.StatusCode, Status: resp.Status}
return misc.StatusCodeError{Code: resp.StatusCode, Status: resp.Status}
}

return nil
Expand Down
6 changes: 4 additions & 2 deletions misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"sync"
"testing"

"github.com/slack-go/slack/internal/misc"

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

Expand Down Expand Up @@ -92,8 +94,8 @@ func TestParseResponseInvalidToken(t *testing.T) {
func TestRetryable(t *testing.T) {
for _, e := range []error{
&RateLimitedError{},
statusCodeError{Code: http.StatusInternalServerError},
statusCodeError{Code: http.StatusTooManyRequests},
misc.StatusCodeError{Code: http.StatusInternalServerError},
misc.StatusCodeError{Code: http.StatusTooManyRequests},
} {
r, ok := e.(slackutilsx.Retryable)
if !ok {
Expand Down
34 changes: 34 additions & 0 deletions socket_mode.go
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()
}
63 changes: 63 additions & 0 deletions socketmode/client.go
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
}
Loading

0 comments on commit 483202e

Please sign in to comment.