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

Socket Mode support #885

Merged
merged 57 commits into from
Jan 19, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
2873e9b
WIP: Socket Mode support
mumoshu Jan 14, 2021
14923f2
Use app token for opening socket mode connection
mumoshu Jan 15, 2021
ff6e556
Move Socket Mode support to the slacksocketmode pkg to avoid import c…
mumoshu Jan 15, 2021
88c913f
Move `backoff` to the internal/backoff pkg for sharing between slack …
mumoshu Jan 15, 2021
493fd6f
Fix the comment on ack method
mumoshu Jan 15, 2021
d472ffd
Remove invalid comment left due to copy-paste on SocketModeClient.con…
mumoshu Jan 15, 2021
e97de46
Fix SocketModeClient receiver name
mumoshu Jan 15, 2021
f5c72e4
Various renames on Socket Mode related symbols for ease of use and co…
mumoshu Jan 15, 2021
08f68aa
Move statusCodeError into the internal pkg for use from slacksocketmode
mumoshu Jan 16, 2021
155b2bc
The initial version that manually verified to work
mumoshu Jan 16, 2021
cf7f3ca
Fix log message
mumoshu Jan 16, 2021
38b0347
Better debug logs (may be removed before merging)
mumoshu Jan 16, 2021
b53014b
Change socketmode example source file name for consistency
mumoshu Jan 16, 2021
4e25959
Call it a Socket Mode "Request" rather than "Message" or "Event" for …
mumoshu Jan 16, 2021
528677c
More restructuring for readability
mumoshu Jan 16, 2021
5e41380
Refactor socket mode deadman for clarity
mumoshu Jan 16, 2021
1a23793
More Event and Request distinction in code-base
mumoshu Jan 16, 2021
0d6994c
Fix socket mode debug log message
mumoshu Jan 16, 2021
1da2c76
Remove unnecessary call to SetCloseHandler
mumoshu Jan 16, 2021
5b8a218
Send back PONG to Slack via WebSocket in Socket Mode
mumoshu Jan 16, 2021
e5d2f1a
Delete unused handleAck function for slacksocketmode
mumoshu Jan 16, 2021
c269a17
Serialize writes to WebSocket for Socket Mode ACKs
mumoshu Jan 16, 2021
852061e
Rename slacksocketmode to socketmode
mumoshu Jan 16, 2021
11f20f3
Merge internalEvent and externalEvent
mumoshu Jan 16, 2021
3cd2344
Apply goimports
mumoshu Jan 16, 2021
7e8a5a7
Give socketmode Response a dedicated source file
mumoshu Jan 16, 2021
93c9b45
Fix an incorrect envvar name in socketmode example
mumoshu Jan 16, 2021
88722e9
Cover all the possible Socket Mode request types
mumoshu Jan 16, 2021
f0a6dde
This may make govet lint happy
mumoshu Jan 16, 2021
0be002a
Add support for receiving `interactive` and `slash_commands` requests…
mumoshu Jan 16, 2021
24cf166
Force the consumer to explicitly ack the socket mode request and veri…
mumoshu Jan 16, 2021
1265022
Various cleanups
mumoshu Jan 17, 2021
10850e1
Rename pingInterval to maxPingInterval for clarity
mumoshu Jan 17, 2021
ebbe82a
Add missing Debugf func
mumoshu Jan 17, 2021
860b3a2
Various fixes regarding build errors
mumoshu Jan 17, 2021
aa57150
Remove the private variant of Ack func and add comments
mumoshu Jan 17, 2021
679c9a6
Remove unneeded ticker in socket mode message handler
mumoshu Jan 17, 2021
40cedb3
socketmode: Make WebSocket message handling core a bit more side-effe…
mumoshu Jan 17, 2021
ff9e9fd
Distinguish among done vs disconnect vs reconnect
mumoshu Jan 17, 2021
91b8ac7
socket mode: Add support for disconnect and reconnect on request from…
mumoshu Jan 17, 2021
e5a4822
socket mode: Make Run() a bit more easy to read
mumoshu Jan 17, 2021
5a2d8db
socket mode: Make slash-command related event value consistent with r…
mumoshu Jan 17, 2021
18c4fd2
Fix examples/socketmode for invalid member-joined-channel event match…
mumoshu Jan 17, 2021
9335441
socketmode: Add support for Go 1.12
mumoshu Jan 17, 2021
b2bfdf1
More unit tests for socket mode request parsing and some tweaks
mumoshu Jan 18, 2021
eaaf0e0
socketmode: Fix logger for correct line numbers in messages
mumoshu Jan 18, 2021
a606305
socketmode: Remove unused idGen
mumoshu Jan 18, 2021
93df48b
socketmode: Fix comment on Client.Open()
mumoshu Jan 18, 2021
d7a26ac
socketmode: make receiver name "smc"
mumoshu Jan 18, 2021
2cf901c
socketmode: Remove RTM-ness from comments on log messages
mumoshu Jan 18, 2021
2cbdf3b
socketmode: Use EventType constants everywhere
mumoshu Jan 18, 2021
04ead06
socketmode: Update the example with connectino lifecycle events
mumoshu Jan 18, 2021
e9e7822
socketmode: prepare costly log data only on debug mode
mumoshu Jan 18, 2021
c0cd3da
Remove unnecessary commented-out line
mumoshu Jan 18, 2021
fcf2b1e
Fix comment on StartSocketModeContext
mumoshu Jan 18, 2021
0f4bd7d
socketmode: Brush up comment on socketmode.Client
mumoshu Jan 19, 2021
0fbd39a
socketmode: Make internal error private to the pkg as it should
mumoshu Jan 19, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions examples/socketmode/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

import (
"fmt"
"github.com/slack-go/slack/slacksocketmode"
"log"
"os"
"strings"

"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_APP_TOKEN must have the prefix \"xoxb-\".")
mumoshu marked this conversation as resolved.
Show resolved Hide resolved
}

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

client := slacksocketmode.New(api)

go func() {
for evt := range client.IncomingEvents {
eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
if !ok {
fmt.Printf("Ignored %+v\n", evt)

continue
}

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

switch evt.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.MemberJoinedChannel:
ev := eventsAPIEvent.Data.(*slackevents.MemberJoinedChannelEvent)

fmt.Printf("user %q joined to channel %q", ev.User, ev.Channel)
case slackevents.AppMention:
ev := eventsAPIEvent.Data.(*slackevents.AppMentionEvent)

_, _, err := api.PostMessage(ev.Channel, slack.MsgOptionText("hey yo!", false))
if err != nil {
fmt.Printf("failed posting message: %v", err)
}
}
}
}()

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
}
29 changes: 29 additions & 0 deletions internal/misc/misc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package misc

import (
"fmt"
"net/http"
)

// StatusCodeError represents an http response error.
// type httpStatusCode interface { HTTPStatusCode() int } to handle it.
type StatusCodeError struct {
kanata2 marked this conversation as resolved.
Show resolved Hide resolved
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
}

25 changes: 2 additions & 23 deletions misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/slack-go/slack/internal/misc"
"io"
"io/ioutil"
"mime"
Expand Down Expand Up @@ -42,28 +43,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 +291,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
5 changes: 3 additions & 2 deletions misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package slack

import (
"context"
"github.com/slack-go/slack/internal/misc"
"log"
"net/http"
"net/url"
Expand Down Expand Up @@ -92,8 +93,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
83 changes: 83 additions & 0 deletions slacksocketmode/socket_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package slacksocketmode

import (
"context"
"github.com/slack-go/slack"
"net/url"
"sync"
"time"

"github.com/gorilla/websocket"
)

const (
socketModeEventTypeHello = "hello"

websocketDefaultTimeout = 10 * time.Second
defaultMaxPingInterval = 30 * time.Second
)

// Open calls the "apps.connection.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)
}

// 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 RTM 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(rtm *Client) {
rtm.pingInterval = d
rtm.resetDeadman()
}
}

// OptionConnParams installs parameters to embed into the connection URL.
func OptionConnParams(connParams url.Values) Option {
return func(smc *Client) {
smc.connParams = connParams
}
}

// NewRTM returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
func New(api *slack.Client, options ...Option) *Client {
result := &Client{
Client: *api,
IncomingEvents: make(chan SocketModeEvent, 50),
outgoingMessages: make(chan slack.OutgoingMessage, 20),
pingInterval: defaultMaxPingInterval,
pingDeadman: time.NewTimer(deadmanDuration(defaultMaxPingInterval)),
killChannel: make(chan bool),
disconnected: make(chan struct{}),
disconnectedm: &sync.Once{},
idGen: slack.NewSafeID(1),
mu: &sync.Mutex{},
}

for _, opt := range options {
opt(result)
}

return result
}

func deadmanDuration(d time.Duration) time.Duration {
return d * 4
}
Loading