From df4b0c80ac2a45f6042102a45c222e7f56515c79 Mon Sep 17 00:00:00 2001 From: ronan Date: Mon, 29 Mar 2021 20:09:04 +0800 Subject: [PATCH] Support for Slack notifications Slack notifications: remove debug log Slack notifications: code lint Slack notifications: code lint Slack notifications: code lint fix according to comments --- README.md | 7 +- backend/app/cmd/server.go | 12 +- backend/app/notify/slack.go | 102 ++ backend/app/notify/slack_test.go | 170 +++ backend/go.mod | 1 + backend/go.sum | 5 + .../github.com/gorilla/websocket/.gitignore | 25 + .../github.com/gorilla/websocket/AUTHORS | 9 + .../github.com/gorilla/websocket/LICENSE | 22 + .../github.com/gorilla/websocket/README.md | 64 + .../github.com/gorilla/websocket/client.go | 395 ++++++ .../gorilla/websocket/client_clone.go | 16 + .../gorilla/websocket/client_clone_legacy.go | 38 + .../gorilla/websocket/compression.go | 148 ++ .../github.com/gorilla/websocket/conn.go | 1201 +++++++++++++++++ .../gorilla/websocket/conn_write.go | 15 + .../gorilla/websocket/conn_write_legacy.go | 18 + .../github.com/gorilla/websocket/doc.go | 227 ++++ .../github.com/gorilla/websocket/go.mod | 3 + .../github.com/gorilla/websocket/go.sum | 0 .../github.com/gorilla/websocket/join.go | 42 + .../github.com/gorilla/websocket/json.go | 60 + .../github.com/gorilla/websocket/mask.go | 54 + .../github.com/gorilla/websocket/mask_safe.go | 15 + .../github.com/gorilla/websocket/prepared.go | 102 ++ .../github.com/gorilla/websocket/proxy.go | 77 ++ .../github.com/gorilla/websocket/server.go | 363 +++++ .../github.com/gorilla/websocket/trace.go | 19 + .../github.com/gorilla/websocket/trace_17.go | 12 + .../github.com/gorilla/websocket/util.go | 283 ++++ .../gorilla/websocket/x_net_proxy.go | 473 +++++++ .../github.com/slack-go/slack/.gitignore | 3 + .../github.com/slack-go/slack/.golangci.yml | 14 + .../github.com/slack-go/slack/CHANGELOG.md | 103 ++ .../vendor/github.com/slack-go/slack/LICENSE | 23 + .../vendor/github.com/slack-go/slack/Makefile | 36 + .../github.com/slack-go/slack/README.md | 97 ++ .../vendor/github.com/slack-go/slack/TODO.txt | 3 + .../vendor/github.com/slack-go/slack/admin.go | 207 +++ .../vendor/github.com/slack-go/slack/apps.go | 61 + .../github.com/slack-go/slack/attachments.go | 98 ++ .../vendor/github.com/slack-go/slack/auth.go | 40 + .../vendor/github.com/slack-go/slack/block.go | 80 ++ .../github.com/slack-go/slack/block_action.go | 26 + .../slack-go/slack/block_context.go | 32 + .../github.com/slack-go/slack/block_conv.go | 417 ++++++ .../slack-go/slack/block_divider.go | 22 + .../slack-go/slack/block_element.go | 466 +++++++ .../github.com/slack-go/slack/block_file.go | 26 + .../github.com/slack-go/slack/block_header.go | 38 + .../github.com/slack-go/slack/block_image.go | 28 + .../github.com/slack-go/slack/block_input.go | 29 + .../github.com/slack-go/slack/block_object.go | 247 ++++ .../slack-go/slack/block_section.go | 42 + .../slack-go/slack/block_unknown.go | 13 + .../vendor/github.com/slack-go/slack/bots.go | 58 + .../github.com/slack-go/slack/channels.go | 714 ++++++++++ .../vendor/github.com/slack-go/slack/chat.go | 840 ++++++++++++ .../github.com/slack-go/slack/comment.go | 10 + .../github.com/slack-go/slack/conversation.go | 645 +++++++++ .../github.com/slack-go/slack/dialog.go | 120 ++ .../slack-go/slack/dialog_select.go | 101 ++ .../github.com/slack-go/slack/dialog_text.go | 59 + .../vendor/github.com/slack-go/slack/dnd.go | 151 +++ .../vendor/github.com/slack-go/slack/emoji.go | 35 + .../github.com/slack-go/slack/errors.go | 21 + .../vendor/github.com/slack-go/slack/files.go | 410 ++++++ .../vendor/github.com/slack-go/slack/go.mod | 12 + .../vendor/github.com/slack-go/slack/go.sum | 12 + .../github.com/slack-go/slack/groups.go | 574 ++++++++ .../github.com/slack-go/slack/history.go | 37 + .../vendor/github.com/slack-go/slack/im.go | 220 +++ .../vendor/github.com/slack-go/slack/info.go | 468 +++++++ .../github.com/slack-go/slack/interactions.go | 221 +++ .../slack/internal/backoff/backoff.go | 62 + .../slack/internal/errorsx/errorsx.go | 17 + .../slack-go/slack/internal/misc/misc.go | 28 + .../slack-go/slack/internal/timex/timex.go | 18 + .../vendor/github.com/slack-go/slack/item.go | 75 + .../github.com/slack-go/slack/logger.go | 60 + .../vendor/github.com/slack-go/slack/logo.png | Bin 0 -> 52440 bytes .../github.com/slack-go/slack/messageID.go | 30 + .../github.com/slack-go/slack/messages.go | 220 +++ .../vendor/github.com/slack-go/slack/misc.go | 341 +++++ .../vendor/github.com/slack-go/slack/oauth.go | 134 ++ .../github.com/slack-go/slack/pagination.go | 20 + .../vendor/github.com/slack-go/slack/pins.go | 94 ++ .../github.com/slack-go/slack/reactions.go | 270 ++++ .../github.com/slack-go/slack/reminders.go | 104 ++ .../vendor/github.com/slack-go/slack/rtm.go | 131 ++ .../github.com/slack-go/slack/search.go | 156 +++ .../github.com/slack-go/slack/security.go | 108 ++ .../vendor/github.com/slack-go/slack/slack.go | 162 +++ .../slack-go/slack/slackutilsx/slackutilsx.go | 62 + .../vendor/github.com/slack-go/slack/slash.go | 55 + .../github.com/slack-go/slack/socket_mode.go | 34 + .../vendor/github.com/slack-go/slack/stars.go | 263 ++++ .../vendor/github.com/slack-go/slack/team.go | 167 +++ .../github.com/slack-go/slack/usergroups.go | 258 ++++ .../vendor/github.com/slack-go/slack/users.go | 666 +++++++++ .../vendor/github.com/slack-go/slack/views.go | 297 ++++ .../github.com/slack-go/slack/webhooks.go | 30 + .../slack-go/slack/webhooks_go112.go | 34 + .../slack-go/slack/webhooks_go113.go | 33 + .../github.com/slack-go/slack/websocket.go | 103 ++ .../slack-go/slack/websocket_channels.go | 72 + .../slack/websocket_desktop_notification.go | 19 + .../github.com/slack-go/slack/websocket_dm.go | 23 + .../slack-go/slack/websocket_dnd.go | 8 + .../slack-go/slack/websocket_files.go | 49 + .../slack-go/slack/websocket_groups.go | 49 + .../slack-go/slack/websocket_internals.go | 102 ++ .../slack-go/slack/websocket_managed_conn.go | 587 ++++++++ .../slack-go/slack/websocket_misc.go | 141 ++ .../websocket_mobile_in_app_notification.go | 20 + .../slack-go/slack/websocket_pins.go | 16 + .../slack-go/slack/websocket_reactions.go | 25 + .../slack-go/slack/websocket_stars.go | 14 + .../slack-go/slack/websocket_subteam.go | 35 + .../slack-go/slack/websocket_teams.go | 33 + backend/vendor/modules.txt | 10 + docs/latest/_index.md | 1 + docs/latest/slack.md | 40 + 123 files changed, 16110 insertions(+), 3 deletions(-) create mode 100644 backend/app/notify/slack.go create mode 100644 backend/app/notify/slack_test.go create mode 100644 backend/vendor/github.com/gorilla/websocket/.gitignore create mode 100644 backend/vendor/github.com/gorilla/websocket/AUTHORS create mode 100644 backend/vendor/github.com/gorilla/websocket/LICENSE create mode 100644 backend/vendor/github.com/gorilla/websocket/README.md create mode 100644 backend/vendor/github.com/gorilla/websocket/client.go create mode 100644 backend/vendor/github.com/gorilla/websocket/client_clone.go create mode 100644 backend/vendor/github.com/gorilla/websocket/client_clone_legacy.go create mode 100644 backend/vendor/github.com/gorilla/websocket/compression.go create mode 100644 backend/vendor/github.com/gorilla/websocket/conn.go create mode 100644 backend/vendor/github.com/gorilla/websocket/conn_write.go create mode 100644 backend/vendor/github.com/gorilla/websocket/conn_write_legacy.go create mode 100644 backend/vendor/github.com/gorilla/websocket/doc.go create mode 100644 backend/vendor/github.com/gorilla/websocket/go.mod create mode 100644 backend/vendor/github.com/gorilla/websocket/go.sum create mode 100644 backend/vendor/github.com/gorilla/websocket/join.go create mode 100644 backend/vendor/github.com/gorilla/websocket/json.go create mode 100644 backend/vendor/github.com/gorilla/websocket/mask.go create mode 100644 backend/vendor/github.com/gorilla/websocket/mask_safe.go create mode 100644 backend/vendor/github.com/gorilla/websocket/prepared.go create mode 100644 backend/vendor/github.com/gorilla/websocket/proxy.go create mode 100644 backend/vendor/github.com/gorilla/websocket/server.go create mode 100644 backend/vendor/github.com/gorilla/websocket/trace.go create mode 100644 backend/vendor/github.com/gorilla/websocket/trace_17.go create mode 100644 backend/vendor/github.com/gorilla/websocket/util.go create mode 100644 backend/vendor/github.com/gorilla/websocket/x_net_proxy.go create mode 100644 backend/vendor/github.com/slack-go/slack/.gitignore create mode 100644 backend/vendor/github.com/slack-go/slack/.golangci.yml create mode 100644 backend/vendor/github.com/slack-go/slack/CHANGELOG.md create mode 100644 backend/vendor/github.com/slack-go/slack/LICENSE create mode 100644 backend/vendor/github.com/slack-go/slack/Makefile create mode 100644 backend/vendor/github.com/slack-go/slack/README.md create mode 100644 backend/vendor/github.com/slack-go/slack/TODO.txt create mode 100644 backend/vendor/github.com/slack-go/slack/admin.go create mode 100644 backend/vendor/github.com/slack-go/slack/apps.go create mode 100644 backend/vendor/github.com/slack-go/slack/attachments.go create mode 100644 backend/vendor/github.com/slack-go/slack/auth.go create mode 100644 backend/vendor/github.com/slack-go/slack/block.go create mode 100644 backend/vendor/github.com/slack-go/slack/block_action.go create mode 100644 backend/vendor/github.com/slack-go/slack/block_context.go create mode 100644 backend/vendor/github.com/slack-go/slack/block_conv.go create mode 100644 backend/vendor/github.com/slack-go/slack/block_divider.go create mode 100644 backend/vendor/github.com/slack-go/slack/block_element.go create mode 100644 backend/vendor/github.com/slack-go/slack/block_file.go create mode 100644 backend/vendor/github.com/slack-go/slack/block_header.go create mode 100644 backend/vendor/github.com/slack-go/slack/block_image.go create mode 100644 backend/vendor/github.com/slack-go/slack/block_input.go create mode 100644 backend/vendor/github.com/slack-go/slack/block_object.go create mode 100644 backend/vendor/github.com/slack-go/slack/block_section.go create mode 100644 backend/vendor/github.com/slack-go/slack/block_unknown.go create mode 100644 backend/vendor/github.com/slack-go/slack/bots.go create mode 100644 backend/vendor/github.com/slack-go/slack/channels.go create mode 100644 backend/vendor/github.com/slack-go/slack/chat.go create mode 100644 backend/vendor/github.com/slack-go/slack/comment.go create mode 100644 backend/vendor/github.com/slack-go/slack/conversation.go create mode 100644 backend/vendor/github.com/slack-go/slack/dialog.go create mode 100644 backend/vendor/github.com/slack-go/slack/dialog_select.go create mode 100644 backend/vendor/github.com/slack-go/slack/dialog_text.go create mode 100644 backend/vendor/github.com/slack-go/slack/dnd.go create mode 100644 backend/vendor/github.com/slack-go/slack/emoji.go create mode 100644 backend/vendor/github.com/slack-go/slack/errors.go create mode 100644 backend/vendor/github.com/slack-go/slack/files.go create mode 100644 backend/vendor/github.com/slack-go/slack/go.mod create mode 100644 backend/vendor/github.com/slack-go/slack/go.sum create mode 100644 backend/vendor/github.com/slack-go/slack/groups.go create mode 100644 backend/vendor/github.com/slack-go/slack/history.go create mode 100644 backend/vendor/github.com/slack-go/slack/im.go create mode 100644 backend/vendor/github.com/slack-go/slack/info.go create mode 100644 backend/vendor/github.com/slack-go/slack/interactions.go create mode 100644 backend/vendor/github.com/slack-go/slack/internal/backoff/backoff.go create mode 100644 backend/vendor/github.com/slack-go/slack/internal/errorsx/errorsx.go create mode 100644 backend/vendor/github.com/slack-go/slack/internal/misc/misc.go create mode 100644 backend/vendor/github.com/slack-go/slack/internal/timex/timex.go create mode 100644 backend/vendor/github.com/slack-go/slack/item.go create mode 100644 backend/vendor/github.com/slack-go/slack/logger.go create mode 100644 backend/vendor/github.com/slack-go/slack/logo.png create mode 100644 backend/vendor/github.com/slack-go/slack/messageID.go create mode 100644 backend/vendor/github.com/slack-go/slack/messages.go create mode 100644 backend/vendor/github.com/slack-go/slack/misc.go create mode 100644 backend/vendor/github.com/slack-go/slack/oauth.go create mode 100644 backend/vendor/github.com/slack-go/slack/pagination.go create mode 100644 backend/vendor/github.com/slack-go/slack/pins.go create mode 100644 backend/vendor/github.com/slack-go/slack/reactions.go create mode 100644 backend/vendor/github.com/slack-go/slack/reminders.go create mode 100644 backend/vendor/github.com/slack-go/slack/rtm.go create mode 100644 backend/vendor/github.com/slack-go/slack/search.go create mode 100644 backend/vendor/github.com/slack-go/slack/security.go create mode 100644 backend/vendor/github.com/slack-go/slack/slack.go create mode 100644 backend/vendor/github.com/slack-go/slack/slackutilsx/slackutilsx.go create mode 100644 backend/vendor/github.com/slack-go/slack/slash.go create mode 100644 backend/vendor/github.com/slack-go/slack/socket_mode.go create mode 100644 backend/vendor/github.com/slack-go/slack/stars.go create mode 100644 backend/vendor/github.com/slack-go/slack/team.go create mode 100644 backend/vendor/github.com/slack-go/slack/usergroups.go create mode 100644 backend/vendor/github.com/slack-go/slack/users.go create mode 100644 backend/vendor/github.com/slack-go/slack/views.go create mode 100644 backend/vendor/github.com/slack-go/slack/webhooks.go create mode 100644 backend/vendor/github.com/slack-go/slack/webhooks_go112.go create mode 100644 backend/vendor/github.com/slack-go/slack/webhooks_go113.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_channels.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_desktop_notification.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_dm.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_dnd.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_files.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_groups.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_internals.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_managed_conn.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_misc.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_mobile_in_app_notification.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_pins.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_reactions.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_stars.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_subteam.go create mode 100644 backend/vendor/github.com/slack-go/slack/websocket_teams.go create mode 100644 docs/latest/slack.md diff --git a/README.md b/README.md index 2d26ff19..2957393d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ Remark42 is a self-hosted, lightweight, and simple (yet functional) comment engi * Images upload with drag-and-drop * Extractor for recent comments, cross-post * RSS for all comments and each post -* Telegram and email notifications +* Telegram, Slack and email notifications for Admins (get notified for each new comment) +* Email notifications for users (get notified when someone responds to your comment) * Export data to json with automatic backups * No external databases, everything embedded in a single data file * Fully dockerized and can be deployed in a single command @@ -155,11 +156,13 @@ _this is the recommended way to run remark42_ | auth.email.subj | AUTH_EMAIL_SUBJ | `remark42 confirmation` | email subject | | auth.email.content-type | AUTH_EMAIL_CONTENT_TYPE | `text/html` | email content type | | auth.email.template | AUTH_EMAIL_TEMPLATE | none (predefined) | custom email message template file | -| notify.type | NOTIFY_TYPE | none | type of notification (telegram and/or email) | +| notify.type | NOTIFY_TYPE | none | type of notification (telegram, slack and/or email) | | notify.queue | NOTIFY_QUEUE | `100` | size of notification queue | | notify.telegram.token | NOTIFY_TELEGRAM_TOKEN | | telegram token | | notify.telegram.chan | NOTIFY_TELEGRAM_CHAN | | telegram channel | | notify.telegram.timeout | NOTIFY_TELEGRAM_TIMEOUT | `5s` | telegram timeout | +| notify.slack.token | NOTIFY_SLACK_TOKEN | | slack token | +| notify.slack.chan | NOTIFY_SLACK_CHAN | `general` | slack channel | | notify.email.fromAddress | NOTIFY_EMAIL_FROM | | from email address | | notify.email.verification_subj | NOTIFY_EMAIL_VERIFICATION_SUBJ | `Email verification` | verification message subject | | notify.email.notify_admin | NOTIFY_EMAIL_ADMIN | `false` | notify admin on new comments via ADMIN_SHARED_EMAIL | diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index be3bc806..2613ed01 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -198,7 +198,7 @@ type SMTPGroup struct { // NotifyGroup defines options for notification type NotifyGroup struct { - Type []string `long:"type" env:"TYPE" description:"type of notification" choice:"none" choice:"telegram" choice:"email" default:"none" env-delim:","` //nolint + Type []string `long:"type" env:"TYPE" description:"type of notification" choice:"none" choice:"telegram" choice:"email" choice:"slack" default:"none" env-delim:","` //nolint QueueSize int `long:"queue" env:"QUEUE" description:"size of notification queue" default:"100"` Telegram struct { Token string `long:"token" env:"TOKEN" description:"telegram token"` @@ -211,6 +211,10 @@ type NotifyGroup struct { VerificationSubject string `long:"verification_subj" env:"VERIFICATION_SUBJ" description:"verification message subject"` AdminNotifications bool `long:"notify_admin" env:"ADMIN" description:"notify admin on new comments via ADMIN_SHARED_EMAIL"` } `group:"email" namespace:"email" env-namespace:"EMAIL"` + Slack struct { + Token string `long:"token" env:"TOKEN" description:"slack token"` + Channel string `long:"chan" env:"CHAN" description:"slack channel"` + } `group:"slack" namespace:"slack" env-namespace:"SLACK"` } // SSLGroup defines options group for server ssl params @@ -807,6 +811,12 @@ func (s *ServerCommand) makeNotify(dataStore *service.DataStore, authenticator * var destinations []notify.Destination for _, t := range s.Notify.Type { switch t { + case "slack": + slack, err := notify.NewSlack(s.Notify.Slack.Token, s.Notify.Slack.Channel) + if err != nil { + return nil, errors.Wrap(err, "failed to create slack notification destination") + } + destinations = append(destinations, slack) case "telegram": tg, err := notify.NewTelegram(s.Notify.Telegram.Token, s.Notify.Telegram.Channel, s.Notify.Telegram.Timeout, s.Notify.Telegram.API) diff --git a/backend/app/notify/slack.go b/backend/app/notify/slack.go new file mode 100644 index 00000000..0addd3cf --- /dev/null +++ b/backend/app/notify/slack.go @@ -0,0 +1,102 @@ +package notify + +import ( + "context" + "fmt" + + log "github.com/go-pkgz/lgr" + "github.com/pkg/errors" + "github.com/slack-go/slack" +) + +// Slack implements notify.Destination for Slack +type Slack struct { + channelID string + channelName string + client *slack.Client +} + +// NewSlack makes Slack bot for notifications +func NewSlack(token, channelName string, opts ...slack.Option) (*Slack, error) { + + if channelName == "" { + channelName = "general" + } + + client := slack.New(token, opts...) + res := &Slack{client: client, channelName: channelName} + + channelID, err := res.findChannelIDByName(channelName) + if err != nil { + return nil, errors.Wrap(err, "can not find slack channel '"+channelName+"'") + } + + res.channelID = channelID + log.Printf("[DEBUG] create new slack notifier for chan %s", channelID) + + return res, nil +} + +// Send to Slack channel +func (t *Slack) Send(ctx context.Context, req Request) error { + + log.Printf("[DEBUG] send slack notification, comment id %s", req.Comment.ID) + + user := req.Comment.User.Name + if req.Comment.ParentID != "" { + user += " → " + req.parent.User.Name + } + + title := fmt.Sprintf("↦ original comment") + if req.Comment.PostTitle != "" { + title = "↦ " + req.Comment.PostTitle + } + + _, _, err := t.client.PostMessageContext(ctx, t.channelID, + slack.MsgOptionText("New comment from "+user, false), + slack.MsgOptionAttachments( + slack.Attachment{ + TitleLink: req.Comment.Locator.URL + uiNav + req.Comment.ID, + Title: title, + Text: req.Comment.Orig, + }, + ), + ) + + return err + +} + +// SendVerification is not implemented for Slack +func (t *Slack) SendVerification(_ context.Context, _ VerificationRequest) error { + return nil +} + +func (t *Slack) String() string { + return "slack: " + t.channelName + " (" + t.channelID + ")" +} + +func (t *Slack) findChannelIDByName(name string) (string, error) { + + params := slack.GetConversationsParameters{} + for { + + chans, next, err := t.client.GetConversations(¶ms) + if err != nil { + return "", err + } + + for _, channel := range chans { + if channel.Name == name { + return channel.ID, nil + } + } + + if next == "" { + break + } + params.Cursor = next + + } + return "", errors.New("no such channel") +} diff --git a/backend/app/notify/slack_test.go b/backend/app/notify/slack_test.go new file mode 100644 index 00000000..5726b2e4 --- /dev/null +++ b/backend/app/notify/slack_test.go @@ -0,0 +1,170 @@ +package notify + +import ( + "context" + "log" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi" + "github.com/slack-go/slack" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/umputun/remark42/backend/app/store" +) + +func TestSlack_New(t *testing.T) { + + ts := newMockSlackServer() + defer ts.Close() + + tb, err := ts.newClient("general") + assert.NoError(t, err) + assert.NotNil(t, tb) + assert.Equal(t, "C12345678", tb.channelID) + + _, err = ts.newClient("unknown-channel") + require.Error(t, err) + assert.Contains(t, err.Error(), "no such channel") + +} + +func TestSlack_Send(t *testing.T) { + + ts := newMockSlackServer() + defer ts.Close() + + tb, err := ts.newClient("general") + assert.NoError(t, err) + assert.NotNil(t, tb) + + c := store.Comment{Text: "some text", ParentID: "1", ID: "999"} + c.User.Name = "from" + cp := store.Comment{Text: "some parent text"} + cp.User.Name = "to" + + err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) + assert.NoError(t, err) + c.PostTitle = "test title" + err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) + assert.NoError(t, err) + + err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) + assert.NoError(t, err) + c.PostTitle = "[test title]" + err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) + assert.NoError(t, err) + + tb, err = ts.newClient("general") + assert.NoError(t, err) + ts.isServerDown = true + err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) + require.Error(t, err) + assert.Contains(t, err.Error(), "slack server error", "send on broken client") + +} + +func TestSlack_Name(t *testing.T) { + ts := newMockSlackServer() + defer ts.Close() + + tb, err := ts.newClient("general") + assert.NoError(t, err) + assert.NotNil(t, tb) + assert.Equal(t, "slack: general (C12345678)", tb.String()) +} + +func TestSlack_SendVerification(t *testing.T) { + ts := newMockSlackServer() + defer ts.Close() + + tb, err := ts.newClient("general") + assert.NoError(t, err) + assert.NotNil(t, tb) + + err = tb.SendVerification(context.TODO(), VerificationRequest{}) + assert.NoError(t, err) +} + +type mockSlackServer struct { + *httptest.Server + isServerDown bool +} + +func (ts *mockSlackServer) newClient(channelName string) (*Slack, error) { + return NewSlack("any-token", channelName, slack.OptionAPIURL(ts.URL+"/")) +} + +func newMockSlackServer() *mockSlackServer { + + mockServer := mockSlackServer{} + router := chi.NewRouter() + router.Post("/conversations.list", func(w http.ResponseWriter, r *http.Request) { + s := `{ + "ok": true, + "channels": [ + { + "id": "C12345678", + "name": "general", + "is_channel": true, + "is_group": false, + "is_im": false, + "created": 1503888888, + "is_archived": false, + "is_general": false, + "unlinked": 0, + "name_normalized": "random", + "is_shared": false, + "parent_conversation": null, + "creator": "U12345678", + "is_ext_shared": false, + "is_org_shared": false, + "pending_shared": [], + "pending_connected_team_ids": [], + "is_pending_ext_shared": false, + "is_member": false, + "is_private": false, + "is_mpim": false, + "previous_names": [], + "num_members": 1 + } + ], + "response_metadata": { + "next_cursor": "" + } + }` + _, _ = w.Write([]byte(s)) + }) + + router.Post("/chat.postMessage", func(w http.ResponseWriter, r *http.Request) { + + if mockServer.isServerDown { + w.WriteHeader(500) + + } else { + s := `{ + "ok": true, + "channel": "C12345678", + "ts": "1617008342.000100", + "message": { + "type": "message", + "subtype": "bot_message", + "text": "wowo", + "ts": "1617008342.000100", + "username": "slackbot", + "bot_id": "B12345678" + } + }` + _, _ = w.Write([]byte(s)) + } + }) + + router.NotFound(func(w http.ResponseWriter, r *http.Request) { + log.Printf("..... 404 for %s .....\n", r.URL) + }) + + mockServer.Server = httptest.NewServer(router) + return &mockServer +} diff --git a/backend/go.mod b/backend/go.mod index 6057fd2a..5ec971d0 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -28,6 +28,7 @@ require ( github.com/rakyll/statik v0.1.7 github.com/rs/xid v1.2.1 github.com/russross/blackfriday/v2 v2.1.0 + github.com/slack-go/slack v0.8.2 github.com/stretchr/testify v1.6.1 github.com/umputun/go-flags v1.5.1 go.etcd.io/bbolt v1.3.5 diff --git a/backend/go.sum b/backend/go.sum index 2c0f4130..675e464f 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -88,6 +88,7 @@ github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= @@ -133,6 +134,8 @@ github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= @@ -216,6 +219,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/slack-go/slack v0.8.2 h1:D7jNu0AInBfdQ4QyKPtVSp+ZxQes3EzWW17RZ/va4JE= +github.com/slack-go/slack v0.8.2/go.mod h1:FGqNzJBmxIsZURAxh2a8D21AnOVvvXZvGligs4npPUM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= diff --git a/backend/vendor/github.com/gorilla/websocket/.gitignore b/backend/vendor/github.com/gorilla/websocket/.gitignore new file mode 100644 index 00000000..cd3fcd1e --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/.gitignore @@ -0,0 +1,25 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe + +.idea/ +*.iml diff --git a/backend/vendor/github.com/gorilla/websocket/AUTHORS b/backend/vendor/github.com/gorilla/websocket/AUTHORS new file mode 100644 index 00000000..1931f400 --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/AUTHORS @@ -0,0 +1,9 @@ +# This is the official list of Gorilla WebSocket authors for copyright +# purposes. +# +# Please keep the list sorted. + +Gary Burd +Google LLC (https://opensource.google.com/) +Joachim Bauch + diff --git a/backend/vendor/github.com/gorilla/websocket/LICENSE b/backend/vendor/github.com/gorilla/websocket/LICENSE new file mode 100644 index 00000000..9171c972 --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/backend/vendor/github.com/gorilla/websocket/README.md b/backend/vendor/github.com/gorilla/websocket/README.md new file mode 100644 index 00000000..19aa2e75 --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/README.md @@ -0,0 +1,64 @@ +# Gorilla WebSocket + +[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket) +[![CircleCI](https://circleci.com/gh/gorilla/websocket.svg?style=svg)](https://circleci.com/gh/gorilla/websocket) + +Gorilla WebSocket is a [Go](http://golang.org/) implementation of the +[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. + +### Documentation + +* [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc) +* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat) +* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command) +* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo) +* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch) + +### Status + +The Gorilla WebSocket package provides a complete and tested implementation of +the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The +package API is stable. + +### Installation + + go get github.com/gorilla/websocket + +### Protocol Compliance + +The Gorilla WebSocket package passes the server tests in the [Autobahn Test +Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn +subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn). + +### Gorilla WebSocket compared with other packages + + + + + + + + + + + + + + + + + + +
github.com/gorillagolang.org/x/net
RFC 6455 Features
Passes Autobahn Test SuiteYesNo
Receive fragmented messageYesNo, see note 1
Send close messageYesNo
Send pings and receive pongsYesNo
Get the type of a received data messageYesYes, see note 2
Other Features
Compression ExtensionsExperimentalNo
Read message using io.ReaderYesNo, see note 3
Write message using io.WriteCloserYesNo, see note 3
+ +Notes: + +1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html). +2. The application can get the type of a received data message by implementing + a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal) + function. +3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries. + Read returns when the input buffer is full or a frame boundary is + encountered. Each call to Write sends a single frame message. The Gorilla + io.Reader and io.WriteCloser operate on a single WebSocket message. + diff --git a/backend/vendor/github.com/gorilla/websocket/client.go b/backend/vendor/github.com/gorilla/websocket/client.go new file mode 100644 index 00000000..962c06a3 --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/client.go @@ -0,0 +1,395 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httptrace" + "net/url" + "strings" + "time" +) + +// ErrBadHandshake is returned when the server response to opening handshake is +// invalid. +var ErrBadHandshake = errors.New("websocket: bad handshake") + +var errInvalidCompression = errors.New("websocket: invalid compression negotiation") + +// NewClient creates a new client connection using the given net connection. +// The URL u specifies the host and request URI. Use requestHeader to specify +// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies +// (Cookie). Use the response.Header to get the selected subprotocol +// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etc. +// +// Deprecated: Use Dialer instead. +func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) { + d := Dialer{ + ReadBufferSize: readBufSize, + WriteBufferSize: writeBufSize, + NetDial: func(net, addr string) (net.Conn, error) { + return netConn, nil + }, + } + return d.Dial(u.String(), requestHeader) +} + +// A Dialer contains options for connecting to WebSocket server. +type Dialer struct { + // NetDial specifies the dial function for creating TCP connections. If + // NetDial is nil, net.Dial is used. + NetDial func(network, addr string) (net.Conn, error) + + // NetDialContext specifies the dial function for creating TCP connections. If + // NetDialContext is nil, net.DialContext is used. + NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error) + + // Proxy specifies a function to return a proxy for a given + // Request. If the function returns a non-nil error, the + // request is aborted with the provided error. + // If Proxy is nil or returns a nil *URL, no proxy is used. + Proxy func(*http.Request) (*url.URL, error) + + // TLSClientConfig specifies the TLS configuration to use with tls.Client. + // If nil, the default configuration is used. + TLSClientConfig *tls.Config + + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + + // ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer + // size is zero, then a useful default size is used. The I/O buffer sizes + // do not limit the size of the messages that can be sent or received. + ReadBufferSize, WriteBufferSize int + + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + + // Subprotocols specifies the client's requested subprotocols. + Subprotocols []string + + // EnableCompression specifies if the client should attempt to negotiate + // per message compression (RFC 7692). Setting this value to true does not + // guarantee that compression will be supported. Currently only "no context + // takeover" modes are supported. + EnableCompression bool + + // Jar specifies the cookie jar. + // If Jar is nil, cookies are not sent in requests and ignored + // in responses. + Jar http.CookieJar +} + +// Dial creates a new client connection by calling DialContext with a background context. +func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + return d.DialContext(context.Background(), urlStr, requestHeader) +} + +var errMalformedURL = errors.New("malformed ws or wss URL") + +func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { + hostPort = u.Host + hostNoPort = u.Host + if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") { + hostNoPort = hostNoPort[:i] + } else { + switch u.Scheme { + case "wss": + hostPort += ":443" + case "https": + hostPort += ":443" + default: + hostPort += ":80" + } + } + return hostPort, hostNoPort +} + +// DefaultDialer is a dialer with all fields set to the default values. +var DefaultDialer = &Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, +} + +// nilDialer is dialer to use when receiver is nil. +var nilDialer = *DefaultDialer + +// DialContext creates a new client connection. Use requestHeader to specify the +// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). +// Use the response.Header to get the selected subprotocol +// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). +// +// The context will be used in the request and in the Dialer. +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etcetera. The response body may not contain the entire response and does not +// need to be closed by the application. +func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + if d == nil { + d = &nilDialer + } + + challengeKey, err := generateChallengeKey() + if err != nil { + return nil, nil, err + } + + u, err := url.Parse(urlStr) + if err != nil { + return nil, nil, err + } + + switch u.Scheme { + case "ws": + u.Scheme = "http" + case "wss": + u.Scheme = "https" + default: + return nil, nil, errMalformedURL + } + + if u.User != nil { + // User name and password are not allowed in websocket URIs. + return nil, nil, errMalformedURL + } + + req := &http.Request{ + Method: "GET", + URL: u, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Host: u.Host, + } + req = req.WithContext(ctx) + + // Set the cookies present in the cookie jar of the dialer + if d.Jar != nil { + for _, cookie := range d.Jar.Cookies(u) { + req.AddCookie(cookie) + } + } + + // Set the request headers using the capitalization for names and values in + // RFC examples. Although the capitalization shouldn't matter, there are + // servers that depend on it. The Header.Set method is not used because the + // method canonicalizes the header names. + req.Header["Upgrade"] = []string{"websocket"} + req.Header["Connection"] = []string{"Upgrade"} + req.Header["Sec-WebSocket-Key"] = []string{challengeKey} + req.Header["Sec-WebSocket-Version"] = []string{"13"} + if len(d.Subprotocols) > 0 { + req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")} + } + for k, vs := range requestHeader { + switch { + case k == "Host": + if len(vs) > 0 { + req.Host = vs[0] + } + case k == "Upgrade" || + k == "Connection" || + k == "Sec-Websocket-Key" || + k == "Sec-Websocket-Version" || + k == "Sec-Websocket-Extensions" || + (k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0): + return nil, nil, errors.New("websocket: duplicate header not allowed: " + k) + case k == "Sec-Websocket-Protocol": + req.Header["Sec-WebSocket-Protocol"] = vs + default: + req.Header[k] = vs + } + } + + if d.EnableCompression { + req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"} + } + + if d.HandshakeTimeout != 0 { + var cancel func() + ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout) + defer cancel() + } + + // Get network dial function. + var netDial func(network, add string) (net.Conn, error) + + if d.NetDialContext != nil { + netDial = func(network, addr string) (net.Conn, error) { + return d.NetDialContext(ctx, network, addr) + } + } else if d.NetDial != nil { + netDial = d.NetDial + } else { + netDialer := &net.Dialer{} + netDial = func(network, addr string) (net.Conn, error) { + return netDialer.DialContext(ctx, network, addr) + } + } + + // If needed, wrap the dial function to set the connection deadline. + if deadline, ok := ctx.Deadline(); ok { + forwardDial := netDial + netDial = func(network, addr string) (net.Conn, error) { + c, err := forwardDial(network, addr) + if err != nil { + return nil, err + } + err = c.SetDeadline(deadline) + if err != nil { + c.Close() + return nil, err + } + return c, nil + } + } + + // If needed, wrap the dial function to connect through a proxy. + if d.Proxy != nil { + proxyURL, err := d.Proxy(req) + if err != nil { + return nil, nil, err + } + if proxyURL != nil { + dialer, err := proxy_FromURL(proxyURL, netDialerFunc(netDial)) + if err != nil { + return nil, nil, err + } + netDial = dialer.Dial + } + } + + hostPort, hostNoPort := hostPortNoPort(u) + trace := httptrace.ContextClientTrace(ctx) + if trace != nil && trace.GetConn != nil { + trace.GetConn(hostPort) + } + + netConn, err := netDial("tcp", hostPort) + if trace != nil && trace.GotConn != nil { + trace.GotConn(httptrace.GotConnInfo{ + Conn: netConn, + }) + } + if err != nil { + return nil, nil, err + } + + defer func() { + if netConn != nil { + netConn.Close() + } + }() + + if u.Scheme == "https" { + cfg := cloneTLSConfig(d.TLSClientConfig) + if cfg.ServerName == "" { + cfg.ServerName = hostNoPort + } + tlsConn := tls.Client(netConn, cfg) + netConn = tlsConn + + var err error + if trace != nil { + err = doHandshakeWithTrace(trace, tlsConn, cfg) + } else { + err = doHandshake(tlsConn, cfg) + } + + if err != nil { + return nil, nil, err + } + } + + conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil) + + if err := req.Write(netConn); err != nil { + return nil, nil, err + } + + if trace != nil && trace.GotFirstResponseByte != nil { + if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 { + trace.GotFirstResponseByte() + } + } + + resp, err := http.ReadResponse(conn.br, req) + if err != nil { + return nil, nil, err + } + + if d.Jar != nil { + if rc := resp.Cookies(); len(rc) > 0 { + d.Jar.SetCookies(u, rc) + } + } + + if resp.StatusCode != 101 || + !strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") || + !strings.EqualFold(resp.Header.Get("Connection"), "upgrade") || + resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) { + // Before closing the network connection on return from this + // function, slurp up some of the response to aid application + // debugging. + buf := make([]byte, 1024) + n, _ := io.ReadFull(resp.Body, buf) + resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n])) + return nil, resp, ErrBadHandshake + } + + for _, ext := range parseExtensions(resp.Header) { + if ext[""] != "permessage-deflate" { + continue + } + _, snct := ext["server_no_context_takeover"] + _, cnct := ext["client_no_context_takeover"] + if !snct || !cnct { + return nil, resp, errInvalidCompression + } + conn.newCompressionWriter = compressNoContextTakeover + conn.newDecompressionReader = decompressNoContextTakeover + break + } + + resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) + conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol") + + netConn.SetDeadline(time.Time{}) + netConn = nil // to avoid close in defer. + return conn, resp, nil +} + +func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error { + if err := tlsConn.Handshake(); err != nil { + return err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return err + } + } + return nil +} diff --git a/backend/vendor/github.com/gorilla/websocket/client_clone.go b/backend/vendor/github.com/gorilla/websocket/client_clone.go new file mode 100644 index 00000000..4f0d9437 --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/client_clone.go @@ -0,0 +1,16 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.8 + +package websocket + +import "crypto/tls" + +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &tls.Config{} + } + return cfg.Clone() +} diff --git a/backend/vendor/github.com/gorilla/websocket/client_clone_legacy.go b/backend/vendor/github.com/gorilla/websocket/client_clone_legacy.go new file mode 100644 index 00000000..babb007f --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/client_clone_legacy.go @@ -0,0 +1,38 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !go1.8 + +package websocket + +import "crypto/tls" + +// cloneTLSConfig clones all public fields except the fields +// SessionTicketsDisabled and SessionTicketKey. This avoids copying the +// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a +// config in active use. +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &tls.Config{} + } + return &tls.Config{ + Rand: cfg.Rand, + Time: cfg.Time, + Certificates: cfg.Certificates, + NameToCertificate: cfg.NameToCertificate, + GetCertificate: cfg.GetCertificate, + RootCAs: cfg.RootCAs, + NextProtos: cfg.NextProtos, + ServerName: cfg.ServerName, + ClientAuth: cfg.ClientAuth, + ClientCAs: cfg.ClientCAs, + InsecureSkipVerify: cfg.InsecureSkipVerify, + CipherSuites: cfg.CipherSuites, + PreferServerCipherSuites: cfg.PreferServerCipherSuites, + ClientSessionCache: cfg.ClientSessionCache, + MinVersion: cfg.MinVersion, + MaxVersion: cfg.MaxVersion, + CurvePreferences: cfg.CurvePreferences, + } +} diff --git a/backend/vendor/github.com/gorilla/websocket/compression.go b/backend/vendor/github.com/gorilla/websocket/compression.go new file mode 100644 index 00000000..813ffb1e --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/compression.go @@ -0,0 +1,148 @@ +// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "compress/flate" + "errors" + "io" + "strings" + "sync" +) + +const ( + minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6 + maxCompressionLevel = flate.BestCompression + defaultCompressionLevel = 1 +) + +var ( + flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool + flateReaderPool = sync.Pool{New: func() interface{} { + return flate.NewReader(nil) + }} +) + +func decompressNoContextTakeover(r io.Reader) io.ReadCloser { + const tail = + // Add four bytes as specified in RFC + "\x00\x00\xff\xff" + + // Add final block to squelch unexpected EOF error from flate reader. + "\x01\x00\x00\xff\xff" + + fr, _ := flateReaderPool.Get().(io.ReadCloser) + fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil) + return &flateReadWrapper{fr} +} + +func isValidCompressionLevel(level int) bool { + return minCompressionLevel <= level && level <= maxCompressionLevel +} + +func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser { + p := &flateWriterPools[level-minCompressionLevel] + tw := &truncWriter{w: w} + fw, _ := p.Get().(*flate.Writer) + if fw == nil { + fw, _ = flate.NewWriter(tw, level) + } else { + fw.Reset(tw) + } + return &flateWriteWrapper{fw: fw, tw: tw, p: p} +} + +// truncWriter is an io.Writer that writes all but the last four bytes of the +// stream to another io.Writer. +type truncWriter struct { + w io.WriteCloser + n int + p [4]byte +} + +func (w *truncWriter) Write(p []byte) (int, error) { + n := 0 + + // fill buffer first for simplicity. + if w.n < len(w.p) { + n = copy(w.p[w.n:], p) + p = p[n:] + w.n += n + if len(p) == 0 { + return n, nil + } + } + + m := len(p) + if m > len(w.p) { + m = len(w.p) + } + + if nn, err := w.w.Write(w.p[:m]); err != nil { + return n + nn, err + } + + copy(w.p[:], w.p[m:]) + copy(w.p[len(w.p)-m:], p[len(p)-m:]) + nn, err := w.w.Write(p[:len(p)-m]) + return n + nn, err +} + +type flateWriteWrapper struct { + fw *flate.Writer + tw *truncWriter + p *sync.Pool +} + +func (w *flateWriteWrapper) Write(p []byte) (int, error) { + if w.fw == nil { + return 0, errWriteClosed + } + return w.fw.Write(p) +} + +func (w *flateWriteWrapper) Close() error { + if w.fw == nil { + return errWriteClosed + } + err1 := w.fw.Flush() + w.p.Put(w.fw) + w.fw = nil + if w.tw.p != [4]byte{0, 0, 0xff, 0xff} { + return errors.New("websocket: internal error, unexpected bytes at end of flate stream") + } + err2 := w.tw.w.Close() + if err1 != nil { + return err1 + } + return err2 +} + +type flateReadWrapper struct { + fr io.ReadCloser +} + +func (r *flateReadWrapper) Read(p []byte) (int, error) { + if r.fr == nil { + return 0, io.ErrClosedPipe + } + n, err := r.fr.Read(p) + if err == io.EOF { + // Preemptively place the reader back in the pool. This helps with + // scenarios where the application does not call NextReader() soon after + // this final read. + r.Close() + } + return n, err +} + +func (r *flateReadWrapper) Close() error { + if r.fr == nil { + return io.ErrClosedPipe + } + err := r.fr.Close() + flateReaderPool.Put(r.fr) + r.fr = nil + return err +} diff --git a/backend/vendor/github.com/gorilla/websocket/conn.go b/backend/vendor/github.com/gorilla/websocket/conn.go new file mode 100644 index 00000000..ca46d2f7 --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/conn.go @@ -0,0 +1,1201 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "encoding/binary" + "errors" + "io" + "io/ioutil" + "math/rand" + "net" + "strconv" + "sync" + "time" + "unicode/utf8" +) + +const ( + // Frame header byte 0 bits from Section 5.2 of RFC 6455 + finalBit = 1 << 7 + rsv1Bit = 1 << 6 + rsv2Bit = 1 << 5 + rsv3Bit = 1 << 4 + + // Frame header byte 1 bits from Section 5.2 of RFC 6455 + maskBit = 1 << 7 + + maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask + maxControlFramePayloadSize = 125 + + writeWait = time.Second + + defaultReadBufferSize = 4096 + defaultWriteBufferSize = 4096 + + continuationFrame = 0 + noFrame = -1 +) + +// Close codes defined in RFC 6455, section 11.7. +const ( + CloseNormalClosure = 1000 + CloseGoingAway = 1001 + CloseProtocolError = 1002 + CloseUnsupportedData = 1003 + CloseNoStatusReceived = 1005 + CloseAbnormalClosure = 1006 + CloseInvalidFramePayloadData = 1007 + ClosePolicyViolation = 1008 + CloseMessageTooBig = 1009 + CloseMandatoryExtension = 1010 + CloseInternalServerErr = 1011 + CloseServiceRestart = 1012 + CloseTryAgainLater = 1013 + CloseTLSHandshake = 1015 +) + +// The message types are defined in RFC 6455, section 11.8. +const ( + // TextMessage denotes a text data message. The text message payload is + // interpreted as UTF-8 encoded text data. + TextMessage = 1 + + // BinaryMessage denotes a binary data message. + BinaryMessage = 2 + + // CloseMessage denotes a close control message. The optional message + // payload contains a numeric code and text. Use the FormatCloseMessage + // function to format a close message payload. + CloseMessage = 8 + + // PingMessage denotes a ping control message. The optional message payload + // is UTF-8 encoded text. + PingMessage = 9 + + // PongMessage denotes a pong control message. The optional message payload + // is UTF-8 encoded text. + PongMessage = 10 +) + +// ErrCloseSent is returned when the application writes a message to the +// connection after sending a close message. +var ErrCloseSent = errors.New("websocket: close sent") + +// ErrReadLimit is returned when reading a message that is larger than the +// read limit set for the connection. +var ErrReadLimit = errors.New("websocket: read limit exceeded") + +// netError satisfies the net Error interface. +type netError struct { + msg string + temporary bool + timeout bool +} + +func (e *netError) Error() string { return e.msg } +func (e *netError) Temporary() bool { return e.temporary } +func (e *netError) Timeout() bool { return e.timeout } + +// CloseError represents a close message. +type CloseError struct { + // Code is defined in RFC 6455, section 11.7. + Code int + + // Text is the optional text payload. + Text string +} + +func (e *CloseError) Error() string { + s := []byte("websocket: close ") + s = strconv.AppendInt(s, int64(e.Code), 10) + switch e.Code { + case CloseNormalClosure: + s = append(s, " (normal)"...) + case CloseGoingAway: + s = append(s, " (going away)"...) + case CloseProtocolError: + s = append(s, " (protocol error)"...) + case CloseUnsupportedData: + s = append(s, " (unsupported data)"...) + case CloseNoStatusReceived: + s = append(s, " (no status)"...) + case CloseAbnormalClosure: + s = append(s, " (abnormal closure)"...) + case CloseInvalidFramePayloadData: + s = append(s, " (invalid payload data)"...) + case ClosePolicyViolation: + s = append(s, " (policy violation)"...) + case CloseMessageTooBig: + s = append(s, " (message too big)"...) + case CloseMandatoryExtension: + s = append(s, " (mandatory extension missing)"...) + case CloseInternalServerErr: + s = append(s, " (internal server error)"...) + case CloseTLSHandshake: + s = append(s, " (TLS handshake error)"...) + } + if e.Text != "" { + s = append(s, ": "...) + s = append(s, e.Text...) + } + return string(s) +} + +// IsCloseError returns boolean indicating whether the error is a *CloseError +// with one of the specified codes. +func IsCloseError(err error, codes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range codes { + if e.Code == code { + return true + } + } + } + return false +} + +// IsUnexpectedCloseError returns boolean indicating whether the error is a +// *CloseError with a code not in the list of expected codes. +func IsUnexpectedCloseError(err error, expectedCodes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range expectedCodes { + if e.Code == code { + return false + } + } + return true + } + return false +} + +var ( + errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true, temporary: true} + errUnexpectedEOF = &CloseError{Code: CloseAbnormalClosure, Text: io.ErrUnexpectedEOF.Error()} + errBadWriteOpCode = errors.New("websocket: bad write message type") + errWriteClosed = errors.New("websocket: write closed") + errInvalidControlFrame = errors.New("websocket: invalid control frame") +) + +func newMaskKey() [4]byte { + n := rand.Uint32() + return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)} +} + +func hideTempErr(err error) error { + if e, ok := err.(net.Error); ok && e.Temporary() { + err = &netError{msg: e.Error(), timeout: e.Timeout()} + } + return err +} + +func isControl(frameType int) bool { + return frameType == CloseMessage || frameType == PingMessage || frameType == PongMessage +} + +func isData(frameType int) bool { + return frameType == TextMessage || frameType == BinaryMessage +} + +var validReceivedCloseCodes = map[int]bool{ + // see http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number + + CloseNormalClosure: true, + CloseGoingAway: true, + CloseProtocolError: true, + CloseUnsupportedData: true, + CloseNoStatusReceived: false, + CloseAbnormalClosure: false, + CloseInvalidFramePayloadData: true, + ClosePolicyViolation: true, + CloseMessageTooBig: true, + CloseMandatoryExtension: true, + CloseInternalServerErr: true, + CloseServiceRestart: true, + CloseTryAgainLater: true, + CloseTLSHandshake: false, +} + +func isValidReceivedCloseCode(code int) bool { + return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999) +} + +// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this +// interface. The type of the value stored in a pool is not specified. +type BufferPool interface { + // Get gets a value from the pool or returns nil if the pool is empty. + Get() interface{} + // Put adds a value to the pool. + Put(interface{}) +} + +// writePoolData is the type added to the write buffer pool. This wrapper is +// used to prevent applications from peeking at and depending on the values +// added to the pool. +type writePoolData struct{ buf []byte } + +// The Conn type represents a WebSocket connection. +type Conn struct { + conn net.Conn + isServer bool + subprotocol string + + // Write fields + mu chan struct{} // used as mutex to protect write to conn + writeBuf []byte // frame is constructed in this buffer. + writePool BufferPool + writeBufSize int + writeDeadline time.Time + writer io.WriteCloser // the current writer returned to the application + isWriting bool // for best-effort concurrent write detection + + writeErrMu sync.Mutex + writeErr error + + enableWriteCompression bool + compressionLevel int + newCompressionWriter func(io.WriteCloser, int) io.WriteCloser + + // Read fields + reader io.ReadCloser // the current reader returned to the application + readErr error + br *bufio.Reader + // bytes remaining in current frame. + // set setReadRemaining to safely update this value and prevent overflow + readRemaining int64 + readFinal bool // true the current message has more frames. + readLength int64 // Message size. + readLimit int64 // Maximum message size. + readMaskPos int + readMaskKey [4]byte + handlePong func(string) error + handlePing func(string) error + handleClose func(int, string) error + readErrCount int + messageReader *messageReader // the current low-level reader + + readDecompress bool // whether last read frame had RSV1 set + newDecompressionReader func(io.Reader) io.ReadCloser +} + +func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn { + + if br == nil { + if readBufferSize == 0 { + readBufferSize = defaultReadBufferSize + } else if readBufferSize < maxControlFramePayloadSize { + // must be large enough for control frame + readBufferSize = maxControlFramePayloadSize + } + br = bufio.NewReaderSize(conn, readBufferSize) + } + + if writeBufferSize <= 0 { + writeBufferSize = defaultWriteBufferSize + } + writeBufferSize += maxFrameHeaderSize + + if writeBuf == nil && writeBufferPool == nil { + writeBuf = make([]byte, writeBufferSize) + } + + mu := make(chan struct{}, 1) + mu <- struct{}{} + c := &Conn{ + isServer: isServer, + br: br, + conn: conn, + mu: mu, + readFinal: true, + writeBuf: writeBuf, + writePool: writeBufferPool, + writeBufSize: writeBufferSize, + enableWriteCompression: true, + compressionLevel: defaultCompressionLevel, + } + c.SetCloseHandler(nil) + c.SetPingHandler(nil) + c.SetPongHandler(nil) + return c +} + +// setReadRemaining tracks the number of bytes remaining on the connection. If n +// overflows, an ErrReadLimit is returned. +func (c *Conn) setReadRemaining(n int64) error { + if n < 0 { + return ErrReadLimit + } + + c.readRemaining = n + return nil +} + +// Subprotocol returns the negotiated protocol for the connection. +func (c *Conn) Subprotocol() string { + return c.subprotocol +} + +// Close closes the underlying network connection without sending or waiting +// for a close message. +func (c *Conn) Close() error { + return c.conn.Close() +} + +// LocalAddr returns the local network address. +func (c *Conn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +// RemoteAddr returns the remote network address. +func (c *Conn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +// Write methods + +func (c *Conn) writeFatal(err error) error { + err = hideTempErr(err) + c.writeErrMu.Lock() + if c.writeErr == nil { + c.writeErr = err + } + c.writeErrMu.Unlock() + return err +} + +func (c *Conn) read(n int) ([]byte, error) { + p, err := c.br.Peek(n) + if err == io.EOF { + err = errUnexpectedEOF + } + c.br.Discard(len(p)) + return p, err +} + +func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error { + <-c.mu + defer func() { c.mu <- struct{}{} }() + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err + } + + c.conn.SetWriteDeadline(deadline) + if len(buf1) == 0 { + _, err = c.conn.Write(buf0) + } else { + err = c.writeBufs(buf0, buf1) + } + if err != nil { + return c.writeFatal(err) + } + if frameType == CloseMessage { + c.writeFatal(ErrCloseSent) + } + return nil +} + +// WriteControl writes a control message with the given deadline. The allowed +// message types are CloseMessage, PingMessage and PongMessage. +func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error { + if !isControl(messageType) { + return errBadWriteOpCode + } + if len(data) > maxControlFramePayloadSize { + return errInvalidControlFrame + } + + b0 := byte(messageType) | finalBit + b1 := byte(len(data)) + if !c.isServer { + b1 |= maskBit + } + + buf := make([]byte, 0, maxFrameHeaderSize+maxControlFramePayloadSize) + buf = append(buf, b0, b1) + + if c.isServer { + buf = append(buf, data...) + } else { + key := newMaskKey() + buf = append(buf, key[:]...) + buf = append(buf, data...) + maskBytes(key, 0, buf[6:]) + } + + d := 1000 * time.Hour + if !deadline.IsZero() { + d = deadline.Sub(time.Now()) + if d < 0 { + return errWriteTimeout + } + } + + timer := time.NewTimer(d) + select { + case <-c.mu: + timer.Stop() + case <-timer.C: + return errWriteTimeout + } + defer func() { c.mu <- struct{}{} }() + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err + } + + c.conn.SetWriteDeadline(deadline) + _, err = c.conn.Write(buf) + if err != nil { + return c.writeFatal(err) + } + if messageType == CloseMessage { + c.writeFatal(ErrCloseSent) + } + return err +} + +// beginMessage prepares a connection and message writer for a new message. +func (c *Conn) beginMessage(mw *messageWriter, messageType int) error { + // Close previous writer if not already closed by the application. It's + // probably better to return an error in this situation, but we cannot + // change this without breaking existing applications. + if c.writer != nil { + c.writer.Close() + c.writer = nil + } + + if !isControl(messageType) && !isData(messageType) { + return errBadWriteOpCode + } + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err + } + + mw.c = c + mw.frameType = messageType + mw.pos = maxFrameHeaderSize + + if c.writeBuf == nil { + wpd, ok := c.writePool.Get().(writePoolData) + if ok { + c.writeBuf = wpd.buf + } else { + c.writeBuf = make([]byte, c.writeBufSize) + } + } + return nil +} + +// NextWriter returns a writer for the next message to send. The writer's Close +// method flushes the complete message to the network. +// +// There can be at most one open writer on a connection. NextWriter closes the +// previous writer if the application has not already done so. +// +// All message types (TextMessage, BinaryMessage, CloseMessage, PingMessage and +// PongMessage) are supported. +func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) { + var mw messageWriter + if err := c.beginMessage(&mw, messageType); err != nil { + return nil, err + } + c.writer = &mw + if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) { + w := c.newCompressionWriter(c.writer, c.compressionLevel) + mw.compress = true + c.writer = w + } + return c.writer, nil +} + +type messageWriter struct { + c *Conn + compress bool // whether next call to flushFrame should set RSV1 + pos int // end of data in writeBuf. + frameType int // type of the current frame. + err error +} + +func (w *messageWriter) endMessage(err error) error { + if w.err != nil { + return err + } + c := w.c + w.err = err + c.writer = nil + if c.writePool != nil { + c.writePool.Put(writePoolData{buf: c.writeBuf}) + c.writeBuf = nil + } + return err +} + +// flushFrame writes buffered data and extra as a frame to the network. The +// final argument indicates that this is the last frame in the message. +func (w *messageWriter) flushFrame(final bool, extra []byte) error { + c := w.c + length := w.pos - maxFrameHeaderSize + len(extra) + + // Check for invalid control frames. + if isControl(w.frameType) && + (!final || length > maxControlFramePayloadSize) { + return w.endMessage(errInvalidControlFrame) + } + + b0 := byte(w.frameType) + if final { + b0 |= finalBit + } + if w.compress { + b0 |= rsv1Bit + } + w.compress = false + + b1 := byte(0) + if !c.isServer { + b1 |= maskBit + } + + // Assume that the frame starts at beginning of c.writeBuf. + framePos := 0 + if c.isServer { + // Adjust up if mask not included in the header. + framePos = 4 + } + + switch { + case length >= 65536: + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 127 + binary.BigEndian.PutUint64(c.writeBuf[framePos+2:], uint64(length)) + case length > 125: + framePos += 6 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 126 + binary.BigEndian.PutUint16(c.writeBuf[framePos+2:], uint16(length)) + default: + framePos += 8 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | byte(length) + } + + if !c.isServer { + key := newMaskKey() + copy(c.writeBuf[maxFrameHeaderSize-4:], key[:]) + maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos]) + if len(extra) > 0 { + return w.endMessage(c.writeFatal(errors.New("websocket: internal error, extra used in client mode"))) + } + } + + // Write the buffers to the connection with best-effort detection of + // concurrent writes. See the concurrency section in the package + // documentation for more info. + + if c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = true + + err := c.write(w.frameType, c.writeDeadline, c.writeBuf[framePos:w.pos], extra) + + if !c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = false + + if err != nil { + return w.endMessage(err) + } + + if final { + w.endMessage(errWriteClosed) + return nil + } + + // Setup for next frame. + w.pos = maxFrameHeaderSize + w.frameType = continuationFrame + return nil +} + +func (w *messageWriter) ncopy(max int) (int, error) { + n := len(w.c.writeBuf) - w.pos + if n <= 0 { + if err := w.flushFrame(false, nil); err != nil { + return 0, err + } + n = len(w.c.writeBuf) - w.pos + } + if n > max { + n = max + } + return n, nil +} + +func (w *messageWriter) Write(p []byte) (int, error) { + if w.err != nil { + return 0, w.err + } + + if len(p) > 2*len(w.c.writeBuf) && w.c.isServer { + // Don't buffer large messages. + err := w.flushFrame(false, p) + if err != nil { + return 0, err + } + return len(p), nil + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.pos:], p[:n]) + w.pos += n + p = p[n:] + } + return nn, nil +} + +func (w *messageWriter) WriteString(p string) (int, error) { + if w.err != nil { + return 0, w.err + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.pos:], p[:n]) + w.pos += n + p = p[n:] + } + return nn, nil +} + +func (w *messageWriter) ReadFrom(r io.Reader) (nn int64, err error) { + if w.err != nil { + return 0, w.err + } + for { + if w.pos == len(w.c.writeBuf) { + err = w.flushFrame(false, nil) + if err != nil { + break + } + } + var n int + n, err = r.Read(w.c.writeBuf[w.pos:]) + w.pos += n + nn += int64(n) + if err != nil { + if err == io.EOF { + err = nil + } + break + } + } + return nn, err +} + +func (w *messageWriter) Close() error { + if w.err != nil { + return w.err + } + return w.flushFrame(true, nil) +} + +// WritePreparedMessage writes prepared message into connection. +func (c *Conn) WritePreparedMessage(pm *PreparedMessage) error { + frameType, frameData, err := pm.frame(prepareKey{ + isServer: c.isServer, + compress: c.newCompressionWriter != nil && c.enableWriteCompression && isData(pm.messageType), + compressionLevel: c.compressionLevel, + }) + if err != nil { + return err + } + if c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = true + err = c.write(frameType, c.writeDeadline, frameData, nil) + if !c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = false + return err +} + +// WriteMessage is a helper method for getting a writer using NextWriter, +// writing the message and closing the writer. +func (c *Conn) WriteMessage(messageType int, data []byte) error { + + if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) { + // Fast path with no allocations and single frame. + + var mw messageWriter + if err := c.beginMessage(&mw, messageType); err != nil { + return err + } + n := copy(c.writeBuf[mw.pos:], data) + mw.pos += n + data = data[n:] + return mw.flushFrame(true, data) + } + + w, err := c.NextWriter(messageType) + if err != nil { + return err + } + if _, err = w.Write(data); err != nil { + return err + } + return w.Close() +} + +// SetWriteDeadline sets the write deadline on the underlying network +// connection. After a write has timed out, the websocket state is corrupt and +// all future writes will return an error. A zero value for t means writes will +// not time out. +func (c *Conn) SetWriteDeadline(t time.Time) error { + c.writeDeadline = t + return nil +} + +// Read methods + +func (c *Conn) advanceFrame() (int, error) { + // 1. Skip remainder of previous frame. + + if c.readRemaining > 0 { + if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil { + return noFrame, err + } + } + + // 2. Read and parse first two bytes of frame header. + + p, err := c.read(2) + if err != nil { + return noFrame, err + } + + final := p[0]&finalBit != 0 + frameType := int(p[0] & 0xf) + mask := p[1]&maskBit != 0 + c.setReadRemaining(int64(p[1] & 0x7f)) + + c.readDecompress = false + if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 { + c.readDecompress = true + p[0] &^= rsv1Bit + } + + if rsv := p[0] & (rsv1Bit | rsv2Bit | rsv3Bit); rsv != 0 { + return noFrame, c.handleProtocolError("unexpected reserved bits 0x" + strconv.FormatInt(int64(rsv), 16)) + } + + switch frameType { + case CloseMessage, PingMessage, PongMessage: + if c.readRemaining > maxControlFramePayloadSize { + return noFrame, c.handleProtocolError("control frame length > 125") + } + if !final { + return noFrame, c.handleProtocolError("control frame not final") + } + case TextMessage, BinaryMessage: + if !c.readFinal { + return noFrame, c.handleProtocolError("message start before final message frame") + } + c.readFinal = final + case continuationFrame: + if c.readFinal { + return noFrame, c.handleProtocolError("continuation after final message frame") + } + c.readFinal = final + default: + return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType)) + } + + // 3. Read and parse frame length as per + // https://tools.ietf.org/html/rfc6455#section-5.2 + // + // The length of the "Payload data", in bytes: if 0-125, that is the payload + // length. + // - If 126, the following 2 bytes interpreted as a 16-bit unsigned + // integer are the payload length. + // - If 127, the following 8 bytes interpreted as + // a 64-bit unsigned integer (the most significant bit MUST be 0) are the + // payload length. Multibyte length quantities are expressed in network byte + // order. + + switch c.readRemaining { + case 126: + p, err := c.read(2) + if err != nil { + return noFrame, err + } + + if err := c.setReadRemaining(int64(binary.BigEndian.Uint16(p))); err != nil { + return noFrame, err + } + case 127: + p, err := c.read(8) + if err != nil { + return noFrame, err + } + + if err := c.setReadRemaining(int64(binary.BigEndian.Uint64(p))); err != nil { + return noFrame, err + } + } + + // 4. Handle frame masking. + + if mask != c.isServer { + return noFrame, c.handleProtocolError("incorrect mask flag") + } + + if mask { + c.readMaskPos = 0 + p, err := c.read(len(c.readMaskKey)) + if err != nil { + return noFrame, err + } + copy(c.readMaskKey[:], p) + } + + // 5. For text and binary messages, enforce read limit and return. + + if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage { + + c.readLength += c.readRemaining + // Don't allow readLength to overflow in the presence of a large readRemaining + // counter. + if c.readLength < 0 { + return noFrame, ErrReadLimit + } + + if c.readLimit > 0 && c.readLength > c.readLimit { + c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait)) + return noFrame, ErrReadLimit + } + + return frameType, nil + } + + // 6. Read control frame payload. + + var payload []byte + if c.readRemaining > 0 { + payload, err = c.read(int(c.readRemaining)) + c.setReadRemaining(0) + if err != nil { + return noFrame, err + } + if c.isServer { + maskBytes(c.readMaskKey, 0, payload) + } + } + + // 7. Process control frame payload. + + switch frameType { + case PongMessage: + if err := c.handlePong(string(payload)); err != nil { + return noFrame, err + } + case PingMessage: + if err := c.handlePing(string(payload)); err != nil { + return noFrame, err + } + case CloseMessage: + closeCode := CloseNoStatusReceived + closeText := "" + if len(payload) >= 2 { + closeCode = int(binary.BigEndian.Uint16(payload)) + if !isValidReceivedCloseCode(closeCode) { + return noFrame, c.handleProtocolError("invalid close code") + } + closeText = string(payload[2:]) + if !utf8.ValidString(closeText) { + return noFrame, c.handleProtocolError("invalid utf8 payload in close frame") + } + } + if err := c.handleClose(closeCode, closeText); err != nil { + return noFrame, err + } + return noFrame, &CloseError{Code: closeCode, Text: closeText} + } + + return frameType, nil +} + +func (c *Conn) handleProtocolError(message string) error { + c.WriteControl(CloseMessage, FormatCloseMessage(CloseProtocolError, message), time.Now().Add(writeWait)) + return errors.New("websocket: " + message) +} + +// NextReader returns the next data message received from the peer. The +// returned messageType is either TextMessage or BinaryMessage. +// +// There can be at most one open reader on a connection. NextReader discards +// the previous message if the application has not already consumed it. +// +// Applications must break out of the application's read loop when this method +// returns a non-nil error value. Errors returned from this method are +// permanent. Once this method returns a non-nil error, all subsequent calls to +// this method return the same error. +func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { + // Close previous reader, only relevant for decompression. + if c.reader != nil { + c.reader.Close() + c.reader = nil + } + + c.messageReader = nil + c.readLength = 0 + + for c.readErr == nil { + frameType, err := c.advanceFrame() + if err != nil { + c.readErr = hideTempErr(err) + break + } + + if frameType == TextMessage || frameType == BinaryMessage { + c.messageReader = &messageReader{c} + c.reader = c.messageReader + if c.readDecompress { + c.reader = c.newDecompressionReader(c.reader) + } + return frameType, c.reader, nil + } + } + + // Applications that do handle the error returned from this method spin in + // tight loop on connection failure. To help application developers detect + // this error, panic on repeated reads to the failed connection. + c.readErrCount++ + if c.readErrCount >= 1000 { + panic("repeated read on failed websocket connection") + } + + return noFrame, nil, c.readErr +} + +type messageReader struct{ c *Conn } + +func (r *messageReader) Read(b []byte) (int, error) { + c := r.c + if c.messageReader != r { + return 0, io.EOF + } + + for c.readErr == nil { + + if c.readRemaining > 0 { + if int64(len(b)) > c.readRemaining { + b = b[:c.readRemaining] + } + n, err := c.br.Read(b) + c.readErr = hideTempErr(err) + if c.isServer { + c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n]) + } + rem := c.readRemaining + rem -= int64(n) + c.setReadRemaining(rem) + if c.readRemaining > 0 && c.readErr == io.EOF { + c.readErr = errUnexpectedEOF + } + return n, c.readErr + } + + if c.readFinal { + c.messageReader = nil + return 0, io.EOF + } + + frameType, err := c.advanceFrame() + switch { + case err != nil: + c.readErr = hideTempErr(err) + case frameType == TextMessage || frameType == BinaryMessage: + c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") + } + } + + err := c.readErr + if err == io.EOF && c.messageReader == r { + err = errUnexpectedEOF + } + return 0, err +} + +func (r *messageReader) Close() error { + return nil +} + +// ReadMessage is a helper method for getting a reader using NextReader and +// reading from that reader to a buffer. +func (c *Conn) ReadMessage() (messageType int, p []byte, err error) { + var r io.Reader + messageType, r, err = c.NextReader() + if err != nil { + return messageType, nil, err + } + p, err = ioutil.ReadAll(r) + return messageType, p, err +} + +// SetReadDeadline sets the read deadline on the underlying network connection. +// After a read has timed out, the websocket connection state is corrupt and +// all future reads will return an error. A zero value for t means reads will +// not time out. +func (c *Conn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +// SetReadLimit sets the maximum size in bytes for a message read from the peer. If a +// message exceeds the limit, the connection sends a close message to the peer +// and returns ErrReadLimit to the application. +func (c *Conn) SetReadLimit(limit int64) { + c.readLimit = limit +} + +// CloseHandler returns the current close handler +func (c *Conn) CloseHandler() func(code int, text string) error { + return c.handleClose +} + +// SetCloseHandler sets the handler for close messages received from the peer. +// The code argument to h is the received close code or CloseNoStatusReceived +// if the close message is empty. The default close handler sends a close +// message back to the peer. +// +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// close messages as described in the section on Control Messages above. +// +// The connection read methods return a CloseError when a close message is +// received. Most applications should handle close messages as part of their +// normal error handling. Applications should only set a close handler when the +// application must perform some action before sending a close message back to +// the peer. +func (c *Conn) SetCloseHandler(h func(code int, text string) error) { + if h == nil { + h = func(code int, text string) error { + message := FormatCloseMessage(code, "") + c.WriteControl(CloseMessage, message, time.Now().Add(writeWait)) + return nil + } + } + c.handleClose = h +} + +// PingHandler returns the current ping handler +func (c *Conn) PingHandler() func(appData string) error { + return c.handlePing +} + +// SetPingHandler sets the handler for ping messages received from the peer. +// The appData argument to h is the PING message application data. The default +// ping handler sends a pong to the peer. +// +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// ping messages as described in the section on Control Messages above. +func (c *Conn) SetPingHandler(h func(appData string) error) { + if h == nil { + h = func(message string) error { + err := c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait)) + if err == ErrCloseSent { + return nil + } else if e, ok := err.(net.Error); ok && e.Temporary() { + return nil + } + return err + } + } + c.handlePing = h +} + +// PongHandler returns the current pong handler +func (c *Conn) PongHandler() func(appData string) error { + return c.handlePong +} + +// SetPongHandler sets the handler for pong messages received from the peer. +// The appData argument to h is the PONG message application data. The default +// pong handler does nothing. +// +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// pong messages as described in the section on Control Messages above. +func (c *Conn) SetPongHandler(h func(appData string) error) { + if h == nil { + h = func(string) error { return nil } + } + c.handlePong = h +} + +// UnderlyingConn returns the internal net.Conn. This can be used to further +// modifications to connection specific flags. +func (c *Conn) UnderlyingConn() net.Conn { + return c.conn +} + +// EnableWriteCompression enables and disables write compression of +// subsequent text and binary messages. This function is a noop if +// compression was not negotiated with the peer. +func (c *Conn) EnableWriteCompression(enable bool) { + c.enableWriteCompression = enable +} + +// SetCompressionLevel sets the flate compression level for subsequent text and +// binary messages. This function is a noop if compression was not negotiated +// with the peer. See the compress/flate package for a description of +// compression levels. +func (c *Conn) SetCompressionLevel(level int) error { + if !isValidCompressionLevel(level) { + return errors.New("websocket: invalid compression level") + } + c.compressionLevel = level + return nil +} + +// FormatCloseMessage formats closeCode and text as a WebSocket close message. +// An empty message is returned for code CloseNoStatusReceived. +func FormatCloseMessage(closeCode int, text string) []byte { + if closeCode == CloseNoStatusReceived { + // Return empty message because it's illegal to send + // CloseNoStatusReceived. Return non-nil value in case application + // checks for nil. + return []byte{} + } + buf := make([]byte, 2+len(text)) + binary.BigEndian.PutUint16(buf, uint16(closeCode)) + copy(buf[2:], text) + return buf +} diff --git a/backend/vendor/github.com/gorilla/websocket/conn_write.go b/backend/vendor/github.com/gorilla/websocket/conn_write.go new file mode 100644 index 00000000..a509a21f --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/conn_write.go @@ -0,0 +1,15 @@ +// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.8 + +package websocket + +import "net" + +func (c *Conn) writeBufs(bufs ...[]byte) error { + b := net.Buffers(bufs) + _, err := b.WriteTo(c.conn) + return err +} diff --git a/backend/vendor/github.com/gorilla/websocket/conn_write_legacy.go b/backend/vendor/github.com/gorilla/websocket/conn_write_legacy.go new file mode 100644 index 00000000..37edaff5 --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/conn_write_legacy.go @@ -0,0 +1,18 @@ +// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !go1.8 + +package websocket + +func (c *Conn) writeBufs(bufs ...[]byte) error { + for _, buf := range bufs { + if len(buf) > 0 { + if _, err := c.conn.Write(buf); err != nil { + return err + } + } + } + return nil +} diff --git a/backend/vendor/github.com/gorilla/websocket/doc.go b/backend/vendor/github.com/gorilla/websocket/doc.go new file mode 100644 index 00000000..8db0cef9 --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/doc.go @@ -0,0 +1,227 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package websocket implements the WebSocket protocol defined in RFC 6455. +// +// Overview +// +// The Conn type represents a WebSocket connection. A server application calls +// the Upgrader.Upgrade method from an HTTP request handler to get a *Conn: +// +// var upgrader = websocket.Upgrader{ +// ReadBufferSize: 1024, +// WriteBufferSize: 1024, +// } +// +// func handler(w http.ResponseWriter, r *http.Request) { +// conn, err := upgrader.Upgrade(w, r, nil) +// if err != nil { +// log.Println(err) +// return +// } +// ... Use conn to send and receive messages. +// } +// +// Call the connection's WriteMessage and ReadMessage methods to send and +// receive messages as a slice of bytes. This snippet of code shows how to echo +// messages using these methods: +// +// for { +// messageType, p, err := conn.ReadMessage() +// if err != nil { +// log.Println(err) +// return +// } +// if err := conn.WriteMessage(messageType, p); err != nil { +// log.Println(err) +// return +// } +// } +// +// In above snippet of code, p is a []byte and messageType is an int with value +// websocket.BinaryMessage or websocket.TextMessage. +// +// An application can also send and receive messages using the io.WriteCloser +// and io.Reader interfaces. To send a message, call the connection NextWriter +// method to get an io.WriteCloser, write the message to the writer and close +// the writer when done. To receive a message, call the connection NextReader +// method to get an io.Reader and read until io.EOF is returned. This snippet +// shows how to echo messages using the NextWriter and NextReader methods: +// +// for { +// messageType, r, err := conn.NextReader() +// if err != nil { +// return +// } +// w, err := conn.NextWriter(messageType) +// if err != nil { +// return err +// } +// if _, err := io.Copy(w, r); err != nil { +// return err +// } +// if err := w.Close(); err != nil { +// return err +// } +// } +// +// Data Messages +// +// The WebSocket protocol distinguishes between text and binary data messages. +// Text messages are interpreted as UTF-8 encoded text. The interpretation of +// binary messages is left to the application. +// +// This package uses the TextMessage and BinaryMessage integer constants to +// identify the two data message types. The ReadMessage and NextReader methods +// return the type of the received message. The messageType argument to the +// WriteMessage and NextWriter methods specifies the type of a sent message. +// +// It is the application's responsibility to ensure that text messages are +// valid UTF-8 encoded text. +// +// Control Messages +// +// The WebSocket protocol defines three types of control messages: close, ping +// and pong. Call the connection WriteControl, WriteMessage or NextWriter +// methods to send a control message to the peer. +// +// Connections handle received close messages by calling the handler function +// set with the SetCloseHandler method and by returning a *CloseError from the +// NextReader, ReadMessage or the message Read method. The default close +// handler sends a close message to the peer. +// +// Connections handle received ping messages by calling the handler function +// set with the SetPingHandler method. The default ping handler sends a pong +// message to the peer. +// +// Connections handle received pong messages by calling the handler function +// set with the SetPongHandler method. The default pong handler does nothing. +// If an application sends ping messages, then the application should set a +// pong handler to receive the corresponding pong. +// +// The control message handler functions are called from the NextReader, +// ReadMessage and message reader Read methods. The default close and ping +// handlers can block these methods for a short time when the handler writes to +// the connection. +// +// The application must read the connection to process close, ping and pong +// messages sent from the peer. If the application is not otherwise interested +// in messages from the peer, then the application should start a goroutine to +// read and discard messages from the peer. A simple example is: +// +// func readLoop(c *websocket.Conn) { +// for { +// if _, _, err := c.NextReader(); err != nil { +// c.Close() +// break +// } +// } +// } +// +// Concurrency +// +// Connections support one concurrent reader and one concurrent writer. +// +// Applications are responsible for ensuring that no more than one goroutine +// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage, +// WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and +// that no more than one goroutine calls the read methods (NextReader, +// SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler) +// concurrently. +// +// The Close and WriteControl methods can be called concurrently with all other +// methods. +// +// Origin Considerations +// +// Web browsers allow Javascript applications to open a WebSocket connection to +// any host. It's up to the server to enforce an origin policy using the Origin +// request header sent by the browser. +// +// The Upgrader calls the function specified in the CheckOrigin field to check +// the origin. If the CheckOrigin function returns false, then the Upgrade +// method fails the WebSocket handshake with HTTP status 403. +// +// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail +// the handshake if the Origin request header is present and the Origin host is +// not equal to the Host request header. +// +// The deprecated package-level Upgrade function does not perform origin +// checking. The application is responsible for checking the Origin header +// before calling the Upgrade function. +// +// Buffers +// +// Connections buffer network input and output to reduce the number +// of system calls when reading or writing messages. +// +// Write buffers are also used for constructing WebSocket frames. See RFC 6455, +// Section 5 for a discussion of message framing. A WebSocket frame header is +// written to the network each time a write buffer is flushed to the network. +// Decreasing the size of the write buffer can increase the amount of framing +// overhead on the connection. +// +// The buffer sizes in bytes are specified by the ReadBufferSize and +// WriteBufferSize fields in the Dialer and Upgrader. The Dialer uses a default +// size of 4096 when a buffer size field is set to zero. The Upgrader reuses +// buffers created by the HTTP server when a buffer size field is set to zero. +// The HTTP server buffers have a size of 4096 at the time of this writing. +// +// The buffer sizes do not limit the size of a message that can be read or +// written by a connection. +// +// Buffers are held for the lifetime of the connection by default. If the +// Dialer or Upgrader WriteBufferPool field is set, then a connection holds the +// write buffer only when writing a message. +// +// Applications should tune the buffer sizes to balance memory use and +// performance. Increasing the buffer size uses more memory, but can reduce the +// number of system calls to read or write the network. In the case of writing, +// increasing the buffer size can reduce the number of frame headers written to +// the network. +// +// Some guidelines for setting buffer parameters are: +// +// Limit the buffer sizes to the maximum expected message size. Buffers larger +// than the largest message do not provide any benefit. +// +// Depending on the distribution of message sizes, setting the buffer size to +// a value less than the maximum expected message size can greatly reduce memory +// use with a small impact on performance. Here's an example: If 99% of the +// messages are smaller than 256 bytes and the maximum message size is 512 +// bytes, then a buffer size of 256 bytes will result in 1.01 more system calls +// than a buffer size of 512 bytes. The memory savings is 50%. +// +// A write buffer pool is useful when the application has a modest number +// writes over a large number of connections. when buffers are pooled, a larger +// buffer size has a reduced impact on total memory use and has the benefit of +// reducing system calls and frame overhead. +// +// Compression EXPERIMENTAL +// +// Per message compression extensions (RFC 7692) are experimentally supported +// by this package in a limited capacity. Setting the EnableCompression option +// to true in Dialer or Upgrader will attempt to negotiate per message deflate +// support. +// +// var upgrader = websocket.Upgrader{ +// EnableCompression: true, +// } +// +// If compression was successfully negotiated with the connection's peer, any +// message received in compressed form will be automatically decompressed. +// All Read methods will return uncompressed bytes. +// +// Per message compression of messages written to a connection can be enabled +// or disabled by calling the corresponding Conn method: +// +// conn.EnableWriteCompression(false) +// +// Currently this package does not support compression with "context takeover". +// This means that messages must be compressed and decompressed in isolation, +// without retaining sliding window or dictionary state across messages. For +// more details refer to RFC 7692. +// +// Use of compression is experimental and may result in decreased performance. +package websocket diff --git a/backend/vendor/github.com/gorilla/websocket/go.mod b/backend/vendor/github.com/gorilla/websocket/go.mod new file mode 100644 index 00000000..1a7afd50 --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/go.mod @@ -0,0 +1,3 @@ +module github.com/gorilla/websocket + +go 1.12 diff --git a/backend/vendor/github.com/gorilla/websocket/go.sum b/backend/vendor/github.com/gorilla/websocket/go.sum new file mode 100644 index 00000000..e69de29b diff --git a/backend/vendor/github.com/gorilla/websocket/join.go b/backend/vendor/github.com/gorilla/websocket/join.go new file mode 100644 index 00000000..c64f8c82 --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/join.go @@ -0,0 +1,42 @@ +// Copyright 2019 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "io" + "strings" +) + +// JoinMessages concatenates received messages to create a single io.Reader. +// The string term is appended to each message. The returned reader does not +// support concurrent calls to the Read method. +func JoinMessages(c *Conn, term string) io.Reader { + return &joinReader{c: c, term: term} +} + +type joinReader struct { + c *Conn + term string + r io.Reader +} + +func (r *joinReader) Read(p []byte) (int, error) { + if r.r == nil { + var err error + _, r.r, err = r.c.NextReader() + if err != nil { + return 0, err + } + if r.term != "" { + r.r = io.MultiReader(r.r, strings.NewReader(r.term)) + } + } + n, err := r.r.Read(p) + if err == io.EOF { + err = nil + r.r = nil + } + return n, err +} diff --git a/backend/vendor/github.com/gorilla/websocket/json.go b/backend/vendor/github.com/gorilla/websocket/json.go new file mode 100644 index 00000000..dc2c1f64 --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/json.go @@ -0,0 +1,60 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "encoding/json" + "io" +) + +// WriteJSON writes the JSON encoding of v as a message. +// +// Deprecated: Use c.WriteJSON instead. +func WriteJSON(c *Conn, v interface{}) error { + return c.WriteJSON(v) +} + +// WriteJSON writes the JSON encoding of v as a message. +// +// See the documentation for encoding/json Marshal for details about the +// conversion of Go values to JSON. +func (c *Conn) WriteJSON(v interface{}) error { + w, err := c.NextWriter(TextMessage) + if err != nil { + return err + } + err1 := json.NewEncoder(w).Encode(v) + err2 := w.Close() + if err1 != nil { + return err1 + } + return err2 +} + +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// Deprecated: Use c.ReadJSON instead. +func ReadJSON(c *Conn, v interface{}) error { + return c.ReadJSON(v) +} + +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// See the documentation for the encoding/json Unmarshal function for details +// about the conversion of JSON to a Go value. +func (c *Conn) ReadJSON(v interface{}) error { + _, r, err := c.NextReader() + if err != nil { + return err + } + err = json.NewDecoder(r).Decode(v) + if err == io.EOF { + // One value is expected in the message. + err = io.ErrUnexpectedEOF + } + return err +} diff --git a/backend/vendor/github.com/gorilla/websocket/mask.go b/backend/vendor/github.com/gorilla/websocket/mask.go new file mode 100644 index 00000000..577fce9e --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/mask.go @@ -0,0 +1,54 @@ +// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in the +// LICENSE file. + +// +build !appengine + +package websocket + +import "unsafe" + +const wordSize = int(unsafe.Sizeof(uintptr(0))) + +func maskBytes(key [4]byte, pos int, b []byte) int { + // Mask one byte at a time for small buffers. + if len(b) < 2*wordSize { + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 + } + + // Mask one byte at a time to word boundary. + if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 { + n = wordSize - n + for i := range b[:n] { + b[i] ^= key[pos&3] + pos++ + } + b = b[n:] + } + + // Create aligned word size key. + var k [wordSize]byte + for i := range k { + k[i] = key[(pos+i)&3] + } + kw := *(*uintptr)(unsafe.Pointer(&k)) + + // Mask one word at a time. + n := (len(b) / wordSize) * wordSize + for i := 0; i < n; i += wordSize { + *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw + } + + // Mask one byte at a time for remaining bytes. + b = b[n:] + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + + return pos & 3 +} diff --git a/backend/vendor/github.com/gorilla/websocket/mask_safe.go b/backend/vendor/github.com/gorilla/websocket/mask_safe.go new file mode 100644 index 00000000..2aac060e --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/mask_safe.go @@ -0,0 +1,15 @@ +// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in the +// LICENSE file. + +// +build appengine + +package websocket + +func maskBytes(key [4]byte, pos int, b []byte) int { + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 +} diff --git a/backend/vendor/github.com/gorilla/websocket/prepared.go b/backend/vendor/github.com/gorilla/websocket/prepared.go new file mode 100644 index 00000000..c854225e --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/prepared.go @@ -0,0 +1,102 @@ +// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bytes" + "net" + "sync" + "time" +) + +// PreparedMessage caches on the wire representations of a message payload. +// Use PreparedMessage to efficiently send a message payload to multiple +// connections. PreparedMessage is especially useful when compression is used +// because the CPU and memory expensive compression operation can be executed +// once for a given set of compression options. +type PreparedMessage struct { + messageType int + data []byte + mu sync.Mutex + frames map[prepareKey]*preparedFrame +} + +// prepareKey defines a unique set of options to cache prepared frames in PreparedMessage. +type prepareKey struct { + isServer bool + compress bool + compressionLevel int +} + +// preparedFrame contains data in wire representation. +type preparedFrame struct { + once sync.Once + data []byte +} + +// NewPreparedMessage returns an initialized PreparedMessage. You can then send +// it to connection using WritePreparedMessage method. Valid wire +// representation will be calculated lazily only once for a set of current +// connection options. +func NewPreparedMessage(messageType int, data []byte) (*PreparedMessage, error) { + pm := &PreparedMessage{ + messageType: messageType, + frames: make(map[prepareKey]*preparedFrame), + data: data, + } + + // Prepare a plain server frame. + _, frameData, err := pm.frame(prepareKey{isServer: true, compress: false}) + if err != nil { + return nil, err + } + + // To protect against caller modifying the data argument, remember the data + // copied to the plain server frame. + pm.data = frameData[len(frameData)-len(data):] + return pm, nil +} + +func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) { + pm.mu.Lock() + frame, ok := pm.frames[key] + if !ok { + frame = &preparedFrame{} + pm.frames[key] = frame + } + pm.mu.Unlock() + + var err error + frame.once.Do(func() { + // Prepare a frame using a 'fake' connection. + // TODO: Refactor code in conn.go to allow more direct construction of + // the frame. + mu := make(chan struct{}, 1) + mu <- struct{}{} + var nc prepareConn + c := &Conn{ + conn: &nc, + mu: mu, + isServer: key.isServer, + compressionLevel: key.compressionLevel, + enableWriteCompression: true, + writeBuf: make([]byte, defaultWriteBufferSize+maxFrameHeaderSize), + } + if key.compress { + c.newCompressionWriter = compressNoContextTakeover + } + err = c.WriteMessage(pm.messageType, pm.data) + frame.data = nc.buf.Bytes() + }) + return pm.messageType, frame.data, err +} + +type prepareConn struct { + buf bytes.Buffer + net.Conn +} + +func (pc *prepareConn) Write(p []byte) (int, error) { return pc.buf.Write(p) } +func (pc *prepareConn) SetWriteDeadline(t time.Time) error { return nil } diff --git a/backend/vendor/github.com/gorilla/websocket/proxy.go b/backend/vendor/github.com/gorilla/websocket/proxy.go new file mode 100644 index 00000000..e87a8c9f --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/proxy.go @@ -0,0 +1,77 @@ +// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "encoding/base64" + "errors" + "net" + "net/http" + "net/url" + "strings" +) + +type netDialerFunc func(network, addr string) (net.Conn, error) + +func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) { + return fn(network, addr) +} + +func init() { + proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) { + return &httpProxyDialer{proxyURL: proxyURL, forwardDial: forwardDialer.Dial}, nil + }) +} + +type httpProxyDialer struct { + proxyURL *url.URL + forwardDial func(network, addr string) (net.Conn, error) +} + +func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) { + hostPort, _ := hostPortNoPort(hpd.proxyURL) + conn, err := hpd.forwardDial(network, hostPort) + if err != nil { + return nil, err + } + + connectHeader := make(http.Header) + if user := hpd.proxyURL.User; user != nil { + proxyUser := user.Username() + if proxyPassword, passwordSet := user.Password(); passwordSet { + credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword)) + connectHeader.Set("Proxy-Authorization", "Basic "+credential) + } + } + + connectReq := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: connectHeader, + } + + if err := connectReq.Write(conn); err != nil { + conn.Close() + return nil, err + } + + // Read response. It's OK to use and discard buffered reader here becaue + // the remote server does not speak until spoken to. + br := bufio.NewReader(conn) + resp, err := http.ReadResponse(br, connectReq) + if err != nil { + conn.Close() + return nil, err + } + + if resp.StatusCode != 200 { + conn.Close() + f := strings.SplitN(resp.Status, " ", 2) + return nil, errors.New(f[1]) + } + return conn, nil +} diff --git a/backend/vendor/github.com/gorilla/websocket/server.go b/backend/vendor/github.com/gorilla/websocket/server.go new file mode 100644 index 00000000..887d5589 --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/server.go @@ -0,0 +1,363 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "errors" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +// HandshakeError describes an error with the handshake from the peer. +type HandshakeError struct { + message string +} + +func (e HandshakeError) Error() string { return e.message } + +// Upgrader specifies parameters for upgrading an HTTP connection to a +// WebSocket connection. +type Upgrader struct { + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + + // ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer + // size is zero, then buffers allocated by the HTTP server are used. The + // I/O buffer sizes do not limit the size of the messages that can be sent + // or received. + ReadBufferSize, WriteBufferSize int + + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + + // Subprotocols specifies the server's supported protocols in order of + // preference. If this field is not nil, then the Upgrade method negotiates a + // subprotocol by selecting the first match in this list with a protocol + // requested by the client. If there's no match, then no protocol is + // negotiated (the Sec-Websocket-Protocol header is not included in the + // handshake response). + Subprotocols []string + + // Error specifies the function for generating HTTP error responses. If Error + // is nil, then http.Error is used to generate the HTTP response. + Error func(w http.ResponseWriter, r *http.Request, status int, reason error) + + // CheckOrigin returns true if the request Origin header is acceptable. If + // CheckOrigin is nil, then a safe default is used: return false if the + // Origin request header is present and the origin host is not equal to + // request Host header. + // + // A CheckOrigin function should carefully validate the request origin to + // prevent cross-site request forgery. + CheckOrigin func(r *http.Request) bool + + // EnableCompression specify if the server should attempt to negotiate per + // message compression (RFC 7692). Setting this value to true does not + // guarantee that compression will be supported. Currently only "no context + // takeover" modes are supported. + EnableCompression bool +} + +func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) { + err := HandshakeError{reason} + if u.Error != nil { + u.Error(w, r, status, err) + } else { + w.Header().Set("Sec-Websocket-Version", "13") + http.Error(w, http.StatusText(status), status) + } + return nil, err +} + +// checkSameOrigin returns true if the origin is not set or is equal to the request host. +func checkSameOrigin(r *http.Request) bool { + origin := r.Header["Origin"] + if len(origin) == 0 { + return true + } + u, err := url.Parse(origin[0]) + if err != nil { + return false + } + return equalASCIIFold(u.Host, r.Host) +} + +func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { + if u.Subprotocols != nil { + clientProtocols := Subprotocols(r) + for _, serverProtocol := range u.Subprotocols { + for _, clientProtocol := range clientProtocols { + if clientProtocol == serverProtocol { + return clientProtocol + } + } + } + } else if responseHeader != nil { + return responseHeader.Get("Sec-Websocket-Protocol") + } + return "" +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// application negotiated subprotocol (Sec-WebSocket-Protocol). +// +// If the upgrade fails, then Upgrade replies to the client with an HTTP error +// response. +func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) { + const badHandshake = "websocket: the client is not using the websocket protocol: " + + if !tokenListContainsValue(r.Header, "Connection", "upgrade") { + return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header") + } + + if !tokenListContainsValue(r.Header, "Upgrade", "websocket") { + return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header") + } + + if r.Method != "GET" { + return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET") + } + + if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") { + return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header") + } + + if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported") + } + + checkOrigin := u.CheckOrigin + if checkOrigin == nil { + checkOrigin = checkSameOrigin + } + if !checkOrigin(r) { + return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin") + } + + challengeKey := r.Header.Get("Sec-Websocket-Key") + if challengeKey == "" { + return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header is missing or blank") + } + + subprotocol := u.selectSubprotocol(r, responseHeader) + + // Negotiate PMCE + var compress bool + if u.EnableCompression { + for _, ext := range parseExtensions(r.Header) { + if ext[""] != "permessage-deflate" { + continue + } + compress = true + break + } + } + + h, ok := w.(http.Hijacker) + if !ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") + } + var brw *bufio.ReadWriter + netConn, brw, err := h.Hijack() + if err != nil { + return u.returnError(w, r, http.StatusInternalServerError, err.Error()) + } + + if brw.Reader.Buffered() > 0 { + netConn.Close() + return nil, errors.New("websocket: client sent data before handshake is complete") + } + + var br *bufio.Reader + if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 { + // Reuse hijacked buffered reader as connection reader. + br = brw.Reader + } + + buf := bufioWriterBuffer(netConn, brw.Writer) + + var writeBuf []byte + if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 { + // Reuse hijacked write buffer as connection buffer. + writeBuf = buf + } + + c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf) + c.subprotocol = subprotocol + + if compress { + c.newCompressionWriter = compressNoContextTakeover + c.newDecompressionReader = decompressNoContextTakeover + } + + // Use larger of hijacked buffer and connection write buffer for header. + p := buf + if len(c.writeBuf) > len(p) { + p = c.writeBuf + } + p = p[:0] + + p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) + p = append(p, computeAcceptKey(challengeKey)...) + p = append(p, "\r\n"...) + if c.subprotocol != "" { + p = append(p, "Sec-WebSocket-Protocol: "...) + p = append(p, c.subprotocol...) + p = append(p, "\r\n"...) + } + if compress { + p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) + } + for k, vs := range responseHeader { + if k == "Sec-Websocket-Protocol" { + continue + } + for _, v := range vs { + p = append(p, k...) + p = append(p, ": "...) + for i := 0; i < len(v); i++ { + b := v[i] + if b <= 31 { + // prevent response splitting. + b = ' ' + } + p = append(p, b) + } + p = append(p, "\r\n"...) + } + } + p = append(p, "\r\n"...) + + // Clear deadlines set by HTTP server. + netConn.SetDeadline(time.Time{}) + + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout)) + } + if _, err = netConn.Write(p); err != nil { + netConn.Close() + return nil, err + } + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Time{}) + } + + return c, nil +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// Deprecated: Use websocket.Upgrader instead. +// +// Upgrade does not perform origin checking. The application is responsible for +// checking the Origin header before calling Upgrade. An example implementation +// of the same origin policy check is: +// +// if req.Header.Get("Origin") != "http://"+req.Host { +// http.Error(w, "Origin not allowed", http.StatusForbidden) +// return +// } +// +// If the endpoint supports subprotocols, then the application is responsible +// for negotiating the protocol used on the connection. Use the Subprotocols() +// function to get the subprotocols requested by the client. Use the +// Sec-Websocket-Protocol response header to specify the subprotocol selected +// by the application. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// negotiated subprotocol (Sec-Websocket-Protocol). +// +// The connection buffers IO to the underlying network connection. The +// readBufSize and writeBufSize parameters specify the size of the buffers to +// use. Messages can be larger than the buffers. +// +// If the request is not a valid WebSocket handshake, then Upgrade returns an +// error of type HandshakeError. Applications should handle this error by +// replying to the client with an HTTP error response. +func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) { + u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize} + u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) { + // don't return errors to maintain backwards compatibility + } + u.CheckOrigin = func(r *http.Request) bool { + // allow all connections by default + return true + } + return u.Upgrade(w, r, responseHeader) +} + +// Subprotocols returns the subprotocols requested by the client in the +// Sec-Websocket-Protocol header. +func Subprotocols(r *http.Request) []string { + h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol")) + if h == "" { + return nil + } + protocols := strings.Split(h, ",") + for i := range protocols { + protocols[i] = strings.TrimSpace(protocols[i]) + } + return protocols +} + +// IsWebSocketUpgrade returns true if the client requested upgrade to the +// WebSocket protocol. +func IsWebSocketUpgrade(r *http.Request) bool { + return tokenListContainsValue(r.Header, "Connection", "upgrade") && + tokenListContainsValue(r.Header, "Upgrade", "websocket") +} + +// bufioReaderSize size returns the size of a bufio.Reader. +func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int { + // This code assumes that peek on a reset reader returns + // bufio.Reader.buf[:0]. + // TODO: Use bufio.Reader.Size() after Go 1.10 + br.Reset(originalReader) + if p, err := br.Peek(0); err == nil { + return cap(p) + } + return 0 +} + +// writeHook is an io.Writer that records the last slice passed to it vio +// io.Writer.Write. +type writeHook struct { + p []byte +} + +func (wh *writeHook) Write(p []byte) (int, error) { + wh.p = p + return len(p), nil +} + +// bufioWriterBuffer grabs the buffer from a bufio.Writer. +func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte { + // This code assumes that bufio.Writer.buf[:1] is passed to the + // bufio.Writer's underlying writer. + var wh writeHook + bw.Reset(&wh) + bw.WriteByte(0) + bw.Flush() + + bw.Reset(originalWriter) + + return wh.p[:cap(wh.p)] +} diff --git a/backend/vendor/github.com/gorilla/websocket/trace.go b/backend/vendor/github.com/gorilla/websocket/trace.go new file mode 100644 index 00000000..834f122a --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/trace.go @@ -0,0 +1,19 @@ +// +build go1.8 + +package websocket + +import ( + "crypto/tls" + "net/http/httptrace" +) + +func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { + if trace.TLSHandshakeStart != nil { + trace.TLSHandshakeStart() + } + err := doHandshake(tlsConn, cfg) + if trace.TLSHandshakeDone != nil { + trace.TLSHandshakeDone(tlsConn.ConnectionState(), err) + } + return err +} diff --git a/backend/vendor/github.com/gorilla/websocket/trace_17.go b/backend/vendor/github.com/gorilla/websocket/trace_17.go new file mode 100644 index 00000000..77d05a0b --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/trace_17.go @@ -0,0 +1,12 @@ +// +build !go1.8 + +package websocket + +import ( + "crypto/tls" + "net/http/httptrace" +) + +func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { + return doHandshake(tlsConn, cfg) +} diff --git a/backend/vendor/github.com/gorilla/websocket/util.go b/backend/vendor/github.com/gorilla/websocket/util.go new file mode 100644 index 00000000..7bf2f66c --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/util.go @@ -0,0 +1,283 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "io" + "net/http" + "strings" + "unicode/utf8" +) + +var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +func computeAcceptKey(challengeKey string) string { + h := sha1.New() + h.Write([]byte(challengeKey)) + h.Write(keyGUID) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +func generateChallengeKey() (string, error) { + p := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, p); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(p), nil +} + +// Token octets per RFC 2616. +var isTokenOctet = [256]bool{ + '!': true, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '*': true, + '+': true, + '-': true, + '.': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'W': true, + 'V': true, + 'X': true, + 'Y': true, + 'Z': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '|': true, + '~': true, +} + +// skipSpace returns a slice of the string s with all leading RFC 2616 linear +// whitespace removed. +func skipSpace(s string) (rest string) { + i := 0 + for ; i < len(s); i++ { + if b := s[i]; b != ' ' && b != '\t' { + break + } + } + return s[i:] +} + +// nextToken returns the leading RFC 2616 token of s and the string following +// the token. +func nextToken(s string) (token, rest string) { + i := 0 + for ; i < len(s); i++ { + if !isTokenOctet[s[i]] { + break + } + } + return s[:i], s[i:] +} + +// nextTokenOrQuoted returns the leading token or quoted string per RFC 2616 +// and the string following the token or quoted string. +func nextTokenOrQuoted(s string) (value string, rest string) { + if !strings.HasPrefix(s, "\"") { + return nextToken(s) + } + s = s[1:] + for i := 0; i < len(s); i++ { + switch s[i] { + case '"': + return s[:i], s[i+1:] + case '\\': + p := make([]byte, len(s)-1) + j := copy(p, s[:i]) + escape := true + for i = i + 1; i < len(s); i++ { + b := s[i] + switch { + case escape: + escape = false + p[j] = b + j++ + case b == '\\': + escape = true + case b == '"': + return string(p[:j]), s[i+1:] + default: + p[j] = b + j++ + } + } + return "", "" + } + } + return "", "" +} + +// equalASCIIFold returns true if s is equal to t with ASCII case folding as +// defined in RFC 4790. +func equalASCIIFold(s, t string) bool { + for s != "" && t != "" { + sr, size := utf8.DecodeRuneInString(s) + s = s[size:] + tr, size := utf8.DecodeRuneInString(t) + t = t[size:] + if sr == tr { + continue + } + if 'A' <= sr && sr <= 'Z' { + sr = sr + 'a' - 'A' + } + if 'A' <= tr && tr <= 'Z' { + tr = tr + 'a' - 'A' + } + if sr != tr { + return false + } + } + return s == t +} + +// tokenListContainsValue returns true if the 1#token header with the given +// name contains a token equal to value with ASCII case folding. +func tokenListContainsValue(header http.Header, name string, value string) bool { +headers: + for _, s := range header[name] { + for { + var t string + t, s = nextToken(skipSpace(s)) + if t == "" { + continue headers + } + s = skipSpace(s) + if s != "" && s[0] != ',' { + continue headers + } + if equalASCIIFold(t, value) { + return true + } + if s == "" { + continue headers + } + s = s[1:] + } + } + return false +} + +// parseExtensions parses WebSocket extensions from a header. +func parseExtensions(header http.Header) []map[string]string { + // From RFC 6455: + // + // Sec-WebSocket-Extensions = extension-list + // extension-list = 1#extension + // extension = extension-token *( ";" extension-param ) + // extension-token = registered-token + // registered-token = token + // extension-param = token [ "=" (token | quoted-string) ] + // ;When using the quoted-string syntax variant, the value + // ;after quoted-string unescaping MUST conform to the + // ;'token' ABNF. + + var result []map[string]string +headers: + for _, s := range header["Sec-Websocket-Extensions"] { + for { + var t string + t, s = nextToken(skipSpace(s)) + if t == "" { + continue headers + } + ext := map[string]string{"": t} + for { + s = skipSpace(s) + if !strings.HasPrefix(s, ";") { + break + } + var k string + k, s = nextToken(skipSpace(s[1:])) + if k == "" { + continue headers + } + s = skipSpace(s) + var v string + if strings.HasPrefix(s, "=") { + v, s = nextTokenOrQuoted(skipSpace(s[1:])) + s = skipSpace(s) + } + if s != "" && s[0] != ',' && s[0] != ';' { + continue headers + } + ext[k] = v + } + if s != "" && s[0] != ',' { + continue headers + } + result = append(result, ext) + if s == "" { + continue headers + } + s = s[1:] + } + } + return result +} diff --git a/backend/vendor/github.com/gorilla/websocket/x_net_proxy.go b/backend/vendor/github.com/gorilla/websocket/x_net_proxy.go new file mode 100644 index 00000000..2e668f6b --- /dev/null +++ b/backend/vendor/github.com/gorilla/websocket/x_net_proxy.go @@ -0,0 +1,473 @@ +// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT. +//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy + +// Package proxy provides support for a variety of protocols to proxy network +// data. +// + +package websocket + +import ( + "errors" + "io" + "net" + "net/url" + "os" + "strconv" + "strings" + "sync" +) + +type proxy_direct struct{} + +// Direct is a direct proxy: one that makes network connections directly. +var proxy_Direct = proxy_direct{} + +func (proxy_direct) Dial(network, addr string) (net.Conn, error) { + return net.Dial(network, addr) +} + +// A PerHost directs connections to a default Dialer unless the host name +// requested matches one of a number of exceptions. +type proxy_PerHost struct { + def, bypass proxy_Dialer + + bypassNetworks []*net.IPNet + bypassIPs []net.IP + bypassZones []string + bypassHosts []string +} + +// NewPerHost returns a PerHost Dialer that directs connections to either +// defaultDialer or bypass, depending on whether the connection matches one of +// the configured rules. +func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost { + return &proxy_PerHost{ + def: defaultDialer, + bypass: bypass, + } +} + +// Dial connects to the address addr on the given network through either +// defaultDialer or bypass. +func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + return p.dialerForRequest(host).Dial(network, addr) +} + +func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer { + if ip := net.ParseIP(host); ip != nil { + for _, net := range p.bypassNetworks { + if net.Contains(ip) { + return p.bypass + } + } + for _, bypassIP := range p.bypassIPs { + if bypassIP.Equal(ip) { + return p.bypass + } + } + return p.def + } + + for _, zone := range p.bypassZones { + if strings.HasSuffix(host, zone) { + return p.bypass + } + if host == zone[1:] { + // For a zone ".example.com", we match "example.com" + // too. + return p.bypass + } + } + for _, bypassHost := range p.bypassHosts { + if bypassHost == host { + return p.bypass + } + } + return p.def +} + +// AddFromString parses a string that contains comma-separated values +// specifying hosts that should use the bypass proxy. Each value is either an +// IP address, a CIDR range, a zone (*.example.com) or a host name +// (localhost). A best effort is made to parse the string and errors are +// ignored. +func (p *proxy_PerHost) AddFromString(s string) { + hosts := strings.Split(s, ",") + for _, host := range hosts { + host = strings.TrimSpace(host) + if len(host) == 0 { + continue + } + if strings.Contains(host, "/") { + // We assume that it's a CIDR address like 127.0.0.0/8 + if _, net, err := net.ParseCIDR(host); err == nil { + p.AddNetwork(net) + } + continue + } + if ip := net.ParseIP(host); ip != nil { + p.AddIP(ip) + continue + } + if strings.HasPrefix(host, "*.") { + p.AddZone(host[1:]) + continue + } + p.AddHost(host) + } +} + +// AddIP specifies an IP address that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match an IP. +func (p *proxy_PerHost) AddIP(ip net.IP) { + p.bypassIPs = append(p.bypassIPs, ip) +} + +// AddNetwork specifies an IP range that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match. +func (p *proxy_PerHost) AddNetwork(net *net.IPNet) { + p.bypassNetworks = append(p.bypassNetworks, net) +} + +// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of +// "example.com" matches "example.com" and all of its subdomains. +func (p *proxy_PerHost) AddZone(zone string) { + if strings.HasSuffix(zone, ".") { + zone = zone[:len(zone)-1] + } + if !strings.HasPrefix(zone, ".") { + zone = "." + zone + } + p.bypassZones = append(p.bypassZones, zone) +} + +// AddHost specifies a host name that will use the bypass proxy. +func (p *proxy_PerHost) AddHost(host string) { + if strings.HasSuffix(host, ".") { + host = host[:len(host)-1] + } + p.bypassHosts = append(p.bypassHosts, host) +} + +// A Dialer is a means to establish a connection. +type proxy_Dialer interface { + // Dial connects to the given address via the proxy. + Dial(network, addr string) (c net.Conn, err error) +} + +// Auth contains authentication parameters that specific Dialers may require. +type proxy_Auth struct { + User, Password string +} + +// FromEnvironment returns the dialer specified by the proxy related variables in +// the environment. +func proxy_FromEnvironment() proxy_Dialer { + allProxy := proxy_allProxyEnv.Get() + if len(allProxy) == 0 { + return proxy_Direct + } + + proxyURL, err := url.Parse(allProxy) + if err != nil { + return proxy_Direct + } + proxy, err := proxy_FromURL(proxyURL, proxy_Direct) + if err != nil { + return proxy_Direct + } + + noProxy := proxy_noProxyEnv.Get() + if len(noProxy) == 0 { + return proxy + } + + perHost := proxy_NewPerHost(proxy, proxy_Direct) + perHost.AddFromString(noProxy) + return perHost +} + +// proxySchemes is a map from URL schemes to a function that creates a Dialer +// from a URL with such a scheme. +var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error) + +// RegisterDialerType takes a URL scheme and a function to generate Dialers from +// a URL with that scheme and a forwarding Dialer. Registered schemes are used +// by FromURL. +func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) { + if proxy_proxySchemes == nil { + proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) + } + proxy_proxySchemes[scheme] = f +} + +// FromURL returns a Dialer given a URL specification and an underlying +// Dialer for it to make network requests. +func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) { + var auth *proxy_Auth + if u.User != nil { + auth = new(proxy_Auth) + auth.User = u.User.Username() + if p, ok := u.User.Password(); ok { + auth.Password = p + } + } + + switch u.Scheme { + case "socks5": + return proxy_SOCKS5("tcp", u.Host, auth, forward) + } + + // If the scheme doesn't match any of the built-in schemes, see if it + // was registered by another package. + if proxy_proxySchemes != nil { + if f, ok := proxy_proxySchemes[u.Scheme]; ok { + return f(u, forward) + } + } + + return nil, errors.New("proxy: unknown scheme: " + u.Scheme) +} + +var ( + proxy_allProxyEnv = &proxy_envOnce{ + names: []string{"ALL_PROXY", "all_proxy"}, + } + proxy_noProxyEnv = &proxy_envOnce{ + names: []string{"NO_PROXY", "no_proxy"}, + } +) + +// envOnce looks up an environment variable (optionally by multiple +// names) once. It mitigates expensive lookups on some platforms +// (e.g. Windows). +// (Borrowed from net/http/transport.go) +type proxy_envOnce struct { + names []string + once sync.Once + val string +} + +func (e *proxy_envOnce) Get() string { + e.once.Do(e.init) + return e.val +} + +func (e *proxy_envOnce) init() { + for _, n := range e.names { + e.val = os.Getenv(n) + if e.val != "" { + return + } + } +} + +// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address +// with an optional username and password. See RFC 1928 and RFC 1929. +func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) { + s := &proxy_socks5{ + network: network, + addr: addr, + forward: forward, + } + if auth != nil { + s.user = auth.User + s.password = auth.Password + } + + return s, nil +} + +type proxy_socks5 struct { + user, password string + network, addr string + forward proxy_Dialer +} + +const proxy_socks5Version = 5 + +const ( + proxy_socks5AuthNone = 0 + proxy_socks5AuthPassword = 2 +) + +const proxy_socks5Connect = 1 + +const ( + proxy_socks5IP4 = 1 + proxy_socks5Domain = 3 + proxy_socks5IP6 = 4 +) + +var proxy_socks5Errors = []string{ + "", + "general failure", + "connection forbidden", + "network unreachable", + "host unreachable", + "connection refused", + "TTL expired", + "command not supported", + "address type not supported", +} + +// Dial connects to the address addr on the given network via the SOCKS5 proxy. +func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) { + switch network { + case "tcp", "tcp6", "tcp4": + default: + return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network) + } + + conn, err := s.forward.Dial(s.network, s.addr) + if err != nil { + return nil, err + } + if err := s.connect(conn, addr); err != nil { + conn.Close() + return nil, err + } + return conn, nil +} + +// connect takes an existing connection to a socks5 proxy server, +// and commands the server to extend that connection to target, +// which must be a canonical address with a host and port. +func (s *proxy_socks5) connect(conn net.Conn, target string) error { + host, portStr, err := net.SplitHostPort(target) + if err != nil { + return err + } + + port, err := strconv.Atoi(portStr) + if err != nil { + return errors.New("proxy: failed to parse port number: " + portStr) + } + if port < 1 || port > 0xffff { + return errors.New("proxy: port number out of range: " + portStr) + } + + // the size here is just an estimate + buf := make([]byte, 0, 6+len(host)) + + buf = append(buf, proxy_socks5Version) + if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 { + buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword) + } else { + buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone) + } + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + if buf[0] != 5 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0]))) + } + if buf[1] == 0xff { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication") + } + + // See RFC 1929 + if buf[1] == proxy_socks5AuthPassword { + buf = buf[:0] + buf = append(buf, 1 /* password protocol version */) + buf = append(buf, uint8(len(s.user))) + buf = append(buf, s.user...) + buf = append(buf, uint8(len(s.password))) + buf = append(buf, s.password...) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if buf[1] != 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password") + } + } + + buf = buf[:0] + buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */) + + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + buf = append(buf, proxy_socks5IP4) + ip = ip4 + } else { + buf = append(buf, proxy_socks5IP6) + } + buf = append(buf, ip...) + } else { + if len(host) > 255 { + return errors.New("proxy: destination host name too long: " + host) + } + buf = append(buf, proxy_socks5Domain) + buf = append(buf, byte(len(host))) + buf = append(buf, host...) + } + buf = append(buf, byte(port>>8), byte(port)) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:4]); err != nil { + return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + failure := "unknown error" + if int(buf[1]) < len(proxy_socks5Errors) { + failure = proxy_socks5Errors[buf[1]] + } + + if len(failure) > 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure) + } + + bytesToDiscard := 0 + switch buf[3] { + case proxy_socks5IP4: + bytesToDiscard = net.IPv4len + case proxy_socks5IP6: + bytesToDiscard = net.IPv6len + case proxy_socks5Domain: + _, err := io.ReadFull(conn, buf[:1]) + if err != nil { + return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + bytesToDiscard = int(buf[0]) + default: + return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr) + } + + if cap(buf) < bytesToDiscard { + buf = make([]byte, bytesToDiscard) + } else { + buf = buf[:bytesToDiscard] + } + if _, err := io.ReadFull(conn, buf); err != nil { + return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + // Also need to discard the port number + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + return nil +} diff --git a/backend/vendor/github.com/slack-go/slack/.gitignore b/backend/vendor/github.com/slack-go/slack/.gitignore new file mode 100644 index 00000000..ac6f3eeb --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/.gitignore @@ -0,0 +1,3 @@ +*.test +*~ +.idea/ diff --git a/backend/vendor/github.com/slack-go/slack/.golangci.yml b/backend/vendor/github.com/slack-go/slack/.golangci.yml new file mode 100644 index 00000000..c16f5389 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/.golangci.yml @@ -0,0 +1,14 @@ +run: + timeout: 6m + issues-exit-code: 1 +linters: + disable-all: true + enable: + - goimports + - govet + - interfacer + - misspell + - structcheck + - unconvert +issues: + new: true diff --git a/backend/vendor/github.com/slack-go/slack/CHANGELOG.md b/backend/vendor/github.com/slack-go/slack/CHANGELOG.md new file mode 100644 index 00000000..32da687b --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/CHANGELOG.md @@ -0,0 +1,103 @@ +### v0.7.0 - October 2, 2020 +full differences can be viewed using `git log --oneline --decorate --color v0.6.6..v0.7.0` +Thank you for many contributions! + +#### Breaking Changes +- Add ScheduledMessage type ([#753]) +- Add description field to option block object ([#783]) +- Fix wrong conditional branch ([#782]) + - The behavior of the user's application may change.(The current behavior is incorrect) + +#### Highlights +- example: fix to start up a server ([#773]) +- example: Add explanation how the message could be sent in a proper way ([#787]) +- example: fix typo in error log ([#779]) +- refactor: Make GetConversationsParameters.ExcludeArchived optional ([#791]) +- refactor: Unify variables to "config" ([#800]) +- refactor: Rename wrong file name ([#810]) +- feature: Add SetUserRealName for change user's realName([#755]) +- feature: Add response metadata to slack response ([#772]) +- feature: Add response metadata to slack response ([#778]) +- feature: Add select block element conversations filter field ([#790]) +- feature: Add Root field to MessageEvent to support thread_broadcast subtype ([#793]) +- feature: Add bot_profile to messages ([#794]) +- doc: Add logo to README ([#813]) +- doc: Update current project status and Add changelog for v0.7.0 ([#814]) + +[#753]: https://github.com/slack-go/slack/pull/753 +[#755]: https://github.com/slack-go/slack/pull/755 +[#772]: https://github.com/slack-go/slack/pull/772 +[#773]: https://github.com/slack-go/slack/pull/773 +[#778]: https://github.com/slack-go/slack/pull/778 +[#779]: https://github.com/slack-go/slack/pull/779 +[#782]: https://github.com/slack-go/slack/pull/782 +[#783]: https://github.com/slack-go/slack/pull/783 +[#787]: https://github.com/slack-go/slack/pull/787 +[#790]: https://github.com/slack-go/slack/pull/790 +[#791]: https://github.com/slack-go/slack/pull/791 +[#793]: https://github.com/slack-go/slack/pull/793 +[#794]: https://github.com/slack-go/slack/pull/794 +[#800]: https://github.com/slack-go/slack/pull/800 +[#810]: https://github.com/slack-go/slack/pull/810 +[#813]: https://github.com/slack-go/slack/pull/813 +[#814]: https://github.com/slack-go/slack/pull/814 + +### v0.6.0 - August 31, 2019 +full differences can be viewed using `git log --oneline --decorate --color v0.5.0..v0.6.0` +thanks to everyone who has contributed since January! + + +#### Breaking Changes: +- Info struct has had fields removed related to deprecated functionality by slack. +- minor adjustments to some structs. +- some internal default values have changed, usually to be more inline with slack defaults or to correct inability to set a particular value. (Message Parse for example.) + +##### Highlights: +- new slacktest package easy mocking for slack client. use, enjoy, please submit PRs for improvements and default behaviours! shamelessly taken from the [slack-test repo](https://github.com/lusis/slack-test) thank you lusis for letting us use it and bring it into the slack repo. +- blocks, blocks, blocks. +- RTM ManagedConnection has undergone a significant cleanup. +in particular handles backoffs gracefully, removed many deadlocks, +and Disconnect is now much more responsive. + +### v0.5.0 - January 20, 2019 +full differences can be viewed using `git log --oneline --decorate --color v0.4.0..v0.5.0` +- Breaking changes: various old struct fields have been removed or updated to match slack's api. +- deadlock fix in RTM disconnect. + +### v0.4.0 - October 06, 2018 +full differences can be viewed using `git log --oneline --decorate --color v0.3.0..v0.4.0` +- Breaking Change: renamed ApplyMessageOption, to mark it as unsafe, +this means it may break without warning in the future. +- Breaking: Msg structure files field changed to an array. +- General: implementation for new security headers. +- RTM: deadlock fix between connect/disconnect. +- Events: various new fields added. +- Web: various fixes, new fields exposed, new methods added. +- Interactions: minor additions expect breaking changes in next release for dialogs/button clicks. +- Utils: new methods added. + +### v0.3.0 - July 30, 2018 +full differences can be viewed using `git log --oneline --decorate --color v0.2.0..v0.3.0` +- slack events initial support added. (still considered experimental and undergoing changes, stability not promised) +- vendored depedencies using dep, ensure using up to date tooling before filing issues. +- RTM has improved its ability to identify dead connections and reconnect automatically (worth calling out in case it has unintended side effects). +- bug fixes (various timestamp handling, error handling, RTM locking, etc). + +### v0.2.0 - Feb 10, 2018 + +Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against. + +Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0) + +### v0.1.0 - May 28, 2017 + +This is released before adding context support. +As the used context package is the one from Go 1.7 this will be the last +compatible with Go < 1.7. + +Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0) + +### v0.0.1 - Jul 26, 2015 + +If you just updated from master and it broke your implementation, please +check [0.0.1](https://github.com/nlopes/slack/releases/tag/v0.0.1) diff --git a/backend/vendor/github.com/slack-go/slack/LICENSE b/backend/vendor/github.com/slack-go/slack/LICENSE new file mode 100644 index 00000000..5145171f --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Norberto Lopes +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/backend/vendor/github.com/slack-go/slack/Makefile b/backend/vendor/github.com/slack-go/slack/Makefile new file mode 100644 index 00000000..72796401 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/Makefile @@ -0,0 +1,36 @@ +.PHONY: help deps fmt lint test test-race test-integration + +help: + @echo "" + @echo "Welcome to slack-go/slack make." + @echo "The following commands are available:" + @echo "" + @echo " make deps : Fetch all dependencies" + @echo " make fmt : Run go fmt to fix any formatting issues" + @echo " make lint : Use go vet to check for linting issues" + @echo " make test : Run all short tests" + @echo " make test-race : Run all tests with race condition checking" + @echo " make test-integration : Run all tests without limiting to short" + @echo "" + @echo " make pr-prep : Run this before making a PR to run fmt, lint and tests" + @echo "" + +deps: + @go mod tidy + +fmt: + @go fmt . + +lint: + @go vet . + +test: + @go test -v -count=1 -timeout 300s -short ./... + +test-race: + @go test -v -count=1 -timeout 300s -short -race ./... + +test-integration: + @go test -v -count=1 -timeout 600s ./... + +pr-prep: fmt lint test-race test-integration diff --git a/backend/vendor/github.com/slack-go/slack/README.md b/backend/vendor/github.com/slack-go/slack/README.md new file mode 100644 index 00000000..7be89066 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/README.md @@ -0,0 +1,97 @@ +Slack API in Go [![GoDoc](https://godoc.org/github.com/slack-go/slack?status.svg)](https://godoc.org/github.com/slack-go/slack) [![Build Status](https://travis-ci.org/slack-go/slack.svg)](https://travis-ci.org/slack-go/slack) +=============== +This is the original Slack library for Go created by Norberto Lopes, transferred to a Github organization. + +[![Join the chat at https://gitter.im/go-slack/Lobby](https://badges.gitter.im/go-slack/Lobby.svg)](https://gitter.im/go-slack/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +![logo](logo.png "icon") + +This library supports most if not all of the `api.slack.com` REST +calls, as well as the Real-Time Messaging protocol over websocket, in +a fully managed way. + +## Project Status +There is currently no major version released. +Therefore, minor version releases may include backward incompatible changes. + +See [CHANGELOG.md](https://github.com/slack-go/slack/blob/master/CHANGELOG.md) for more information about the changes. + +## Installing + +### *go get* + + $ go get -u github.com/slack-go/slack + +## Example + +### Getting all groups + +```golang +import ( + "fmt" + + "github.com/slack-go/slack" +) + +func main() { + api := slack.New("YOUR_TOKEN_HERE") + // If you set debugging, it will log all requests to the console + // Useful when encountering issues + // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true)) + groups, err := api.GetGroups(false) + if err != nil { + fmt.Printf("%s\n", err) + return + } + for _, group := range groups { + fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) + } +} +``` + +### Getting User Information + +```golang +import ( + "fmt" + + "github.com/slack-go/slack" +) + +func main() { + api := slack.New("YOUR_TOKEN_HERE") + user, err := api.GetUserInfo("U023BECGF") + if err != nil { + fmt.Printf("%s\n", err) + return + } + fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email) +} +``` + +## Minimal RTM usage: + +See https://github.com/slack-go/slack/blob/master/examples/websocket/websocket.go + + +## Minimal EventsAPI usage: + +See https://github.com/slack-go/slack/blob/master/examples/eventsapi/events.go + + +## Contributing + +You are more than welcome to contribute to this project. Fork and +make a Pull Request, or create an Issue if you see any problem. + +Before making any Pull Request please run the following: + +``` +make pr-prep +``` + +This will check/update code formatting, linting and then run all tests + +## License + +BSD 2 Clause license diff --git a/backend/vendor/github.com/slack-go/slack/TODO.txt b/backend/vendor/github.com/slack-go/slack/TODO.txt new file mode 100644 index 00000000..8607960b --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/TODO.txt @@ -0,0 +1,3 @@ +- Add more tests!!! +- Add support to have markdown hints + - See section Message Formatting at https://api.slack.com/docs/formatting diff --git a/backend/vendor/github.com/slack-go/slack/admin.go b/backend/vendor/github.com/slack-go/slack/admin.go new file mode 100644 index 00000000..d51426b5 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/admin.go @@ -0,0 +1,207 @@ +package slack + +import ( + "context" + "fmt" + "net/url" + "strings" +) + +func (api *Client) adminRequest(ctx context.Context, method string, teamName string, values url.Values) error { + resp := &SlackResponse{} + err := parseAdminResponse(ctx, api.httpclient, method, teamName, values, resp, api) + if err != nil { + return err + } + + return resp.Err() +} + +// DisableUser disabled a user account, given a user ID +func (api *Client) DisableUser(teamName string, uid string) error { + return api.DisableUserContext(context.Background(), teamName, uid) +} + +// DisableUserContext disabled a user account, given a user ID with a custom context +func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid string) error { + values := url.Values{ + "user": {uid}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + if err := api.adminRequest(ctx, "setInactive", teamName, values); err != nil { + return fmt.Errorf("failed to disable user with id '%s': %s", uid, err) + } + + return nil +} + +// InviteGuest invites a user to Slack as a single-channel guest +func (api *Client) InviteGuest(teamName, channel, firstName, lastName, emailAddress string) error { + return api.InviteGuestContext(context.Background(), teamName, channel, firstName, lastName, emailAddress) +} + +// InviteGuestContext invites a user to Slack as a single-channel guest with a custom context +func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, firstName, lastName, emailAddress string) error { + values := url.Values{ + "email": {emailAddress}, + "channels": {channel}, + "first_name": {firstName}, + "last_name": {lastName}, + "ultra_restricted": {"1"}, + "token": {api.token}, + "resend": {"true"}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + err := api.adminRequest(ctx, "invite", teamName, values) + if err != nil { + return fmt.Errorf("Failed to invite single-channel guest: %s", err) + } + + return nil +} + +// InviteRestricted invites a user to Slack as a restricted account +func (api *Client) InviteRestricted(teamName, channel, firstName, lastName, emailAddress string) error { + return api.InviteRestrictedContext(context.Background(), teamName, channel, firstName, lastName, emailAddress) +} + +// InviteRestrictedContext invites a user to Slack as a restricted account with a custom context +func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channel, firstName, lastName, emailAddress string) error { + values := url.Values{ + "email": {emailAddress}, + "channels": {channel}, + "first_name": {firstName}, + "last_name": {lastName}, + "restricted": {"1"}, + "token": {api.token}, + "resend": {"true"}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + err := api.adminRequest(ctx, "invite", teamName, values) + if err != nil { + return fmt.Errorf("Failed to restricted account: %s", err) + } + + return nil +} + +// InviteToTeam invites a user to a Slack team +func (api *Client) InviteToTeam(teamName, firstName, lastName, emailAddress string) error { + return api.InviteToTeamContext(context.Background(), teamName, firstName, lastName, emailAddress) +} + +// InviteToTeamContext invites a user to a Slack team with a custom context +func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName, lastName, emailAddress string) error { + values := url.Values{ + "email": {emailAddress}, + "first_name": {firstName}, + "last_name": {lastName}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + err := api.adminRequest(ctx, "invite", teamName, values) + if err != nil { + return fmt.Errorf("Failed to invite to team: %s", err) + } + + return nil +} + +// SetRegular enables the specified user +func (api *Client) SetRegular(teamName, user string) error { + return api.SetRegularContext(context.Background(), teamName, user) +} + +// SetRegularContext enables the specified user with a custom context +func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) error { + values := url.Values{ + "user": {user}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + err := api.adminRequest(ctx, "setRegular", teamName, values) + if err != nil { + return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err) + } + + return nil +} + +// SendSSOBindingEmail sends an SSO binding email to the specified user +func (api *Client) SendSSOBindingEmail(teamName, user string) error { + return api.SendSSOBindingEmailContext(context.Background(), teamName, user) +} + +// SendSSOBindingEmailContext sends an SSO binding email to the specified user with a custom context +func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, user string) error { + values := url.Values{ + "user": {user}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + err := api.adminRequest(ctx, "sendSSOBind", teamName, values) + if err != nil { + return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err) + } + + return nil +} + +// SetUltraRestricted converts a user into a single-channel guest +func (api *Client) SetUltraRestricted(teamName, uid, channel string) error { + return api.SetUltraRestrictedContext(context.Background(), teamName, uid, channel) +} + +// SetUltraRestrictedContext converts a user into a single-channel guest with a custom context +func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid, channel string) error { + values := url.Values{ + "user": {uid}, + "channel": {channel}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + err := api.adminRequest(ctx, "setUltraRestricted", teamName, values) + if err != nil { + return fmt.Errorf("Failed to ultra-restrict account: %s", err) + } + + return nil +} + +// SetRestricted converts a user into a restricted account +func (api *Client) SetRestricted(teamName, uid string, channelIds ...string) error { + return api.SetRestrictedContext(context.Background(), teamName, uid, channelIds...) +} + +// SetRestrictedContext converts a user into a restricted account with a custom context +func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string, channelIds ...string) error { + values := url.Values{ + "user": {uid}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + "channels": {strings.Join(channelIds, ",")}, + } + + err := api.adminRequest(ctx, "setRestricted", teamName, values) + if err != nil { + return fmt.Errorf("failed to restrict account: %s", err) + } + + return nil +} diff --git a/backend/vendor/github.com/slack-go/slack/apps.go b/backend/vendor/github.com/slack-go/slack/apps.go new file mode 100644 index 00000000..d9749438 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/apps.go @@ -0,0 +1,61 @@ +package slack + +import ( + "context" + "encoding/json" + "net/url" +) + +type listEventAuthorizationsResponse struct { + SlackResponse + Authorizations []EventAuthorization `json:"authorizations"` +} + +type EventAuthorization struct { + EnterpriseID string `json:"enterprise_id"` + TeamID string `json:"team_id"` + UserID string `json:"user_id"` + IsBot bool `json:"is_bot"` + IsEnterpriseInstall bool `json:"is_enterprise_install"` +} + +func (api *Client) ListEventAuthorizations(eventContext string) ([]EventAuthorization, error) { + return api.ListEventAuthorizationsContext(context.Background(), eventContext) +} + +// ListEventAuthorizationsContext lists authed users and teams for the given event_context. You must provide an app-level token to the client using OptionAppLevelToken. More info: https://api.slack.com/methods/apps.event.authorizations.list +func (api *Client) ListEventAuthorizationsContext(ctx context.Context, eventContext string) ([]EventAuthorization, error) { + resp := &listEventAuthorizationsResponse{} + + request, _ := json.Marshal(map[string]string{ + "event_context": eventContext, + }) + + err := postJSON(ctx, api.httpclient, api.endpoint+"apps.event.authorizations.list", api.appLevelToken, request, &resp, api) + + if err != nil { + return nil, err + } + if !resp.Ok { + return nil, resp.Err() + } + + return resp.Authorizations, nil +} + +func (api *Client) UninstallApp(clientID, clientSecret string) error { + values := url.Values{ + "token": {api.token}, + "client_id": {clientID}, + "client_secret": {clientSecret}, + } + + response := SlackResponse{} + + err := api.getMethod(context.Background(), "apps.uninstall", values, &response) + if err != nil { + return err + } + + return response.Err() +} diff --git a/backend/vendor/github.com/slack-go/slack/attachments.go b/backend/vendor/github.com/slack-go/slack/attachments.go new file mode 100644 index 00000000..69b223a2 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/attachments.go @@ -0,0 +1,98 @@ +package slack + +import "encoding/json" + +// AttachmentField contains information for an attachment field +// An Attachment can contain multiple of these +type AttachmentField struct { + Title string `json:"title"` + Value string `json:"value"` + Short bool `json:"short"` +} + +// AttachmentAction is a button or menu to be included in the attachment. Required when +// using message buttons or menus and otherwise not useful. A maximum of 5 actions may be +// provided per attachment. +type AttachmentAction struct { + Name string `json:"name"` // Required. + Text string `json:"text"` // Required. + Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger". + Type actionType `json:"type"` // Required. Must be set to "button" or "select". + Value string `json:"value,omitempty"` // Optional. + DataSource string `json:"data_source,omitempty"` // Optional. + MinQueryLength int `json:"min_query_length,omitempty"` // Optional. Default value is 1. + Options []AttachmentActionOption `json:"options,omitempty"` // Optional. Maximum of 100 options can be provided in each menu. + SelectedOptions []AttachmentActionOption `json:"selected_options,omitempty"` // Optional. The first element of this array will be set as the pre-selected option for this menu. + OptionGroups []AttachmentActionOptionGroup `json:"option_groups,omitempty"` // Optional. + Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional. + URL string `json:"url,omitempty"` // Optional. +} + +// actionType returns the type of the action +func (a AttachmentAction) actionType() actionType { + return a.Type +} + +// AttachmentActionOption the individual option to appear in action menu. +type AttachmentActionOption struct { + Text string `json:"text"` // Required. + Value string `json:"value"` // Required. + Description string `json:"description,omitempty"` // Optional. Up to 30 characters. +} + +// AttachmentActionOptionGroup is a semi-hierarchal way to list available options to appear in action menu. +type AttachmentActionOptionGroup struct { + Text string `json:"text"` // Required. + Options []AttachmentActionOption `json:"options"` // Required. +} + +// AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction) +// DEPRECATED: use InteractionCallback +type AttachmentActionCallback InteractionCallback + +// ConfirmationField are used to ask users to confirm actions +type ConfirmationField struct { + Title string `json:"title,omitempty"` // Optional. + Text string `json:"text"` // Required. + OkText string `json:"ok_text,omitempty"` // Optional. Defaults to "Okay" + DismissText string `json:"dismiss_text,omitempty"` // Optional. Defaults to "Cancel" +} + +// Attachment contains all the information for an attachment +type Attachment struct { + Color string `json:"color,omitempty"` + Fallback string `json:"fallback,omitempty"` + + CallbackID string `json:"callback_id,omitempty"` + ID int `json:"id,omitempty"` + + AuthorID string `json:"author_id,omitempty"` + AuthorName string `json:"author_name,omitempty"` + AuthorSubname string `json:"author_subname,omitempty"` + AuthorLink string `json:"author_link,omitempty"` + AuthorIcon string `json:"author_icon,omitempty"` + + Title string `json:"title,omitempty"` + TitleLink string `json:"title_link,omitempty"` + Pretext string `json:"pretext,omitempty"` + Text string `json:"text,omitempty"` + + ImageURL string `json:"image_url,omitempty"` + ThumbURL string `json:"thumb_url,omitempty"` + + ServiceName string `json:"service_name,omitempty"` + ServiceIcon string `json:"service_icon,omitempty"` + FromURL string `json:"from_url,omitempty"` + OriginalURL string `json:"original_url,omitempty"` + + Fields []AttachmentField `json:"fields,omitempty"` + Actions []AttachmentAction `json:"actions,omitempty"` + MarkdownIn []string `json:"mrkdwn_in,omitempty"` + + Blocks Blocks `json:"blocks,omitempty"` + + Footer string `json:"footer,omitempty"` + FooterIcon string `json:"footer_icon,omitempty"` + + Ts json.Number `json:"ts,omitempty"` +} diff --git a/backend/vendor/github.com/slack-go/slack/auth.go b/backend/vendor/github.com/slack-go/slack/auth.go new file mode 100644 index 00000000..f4f7f003 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/auth.go @@ -0,0 +1,40 @@ +package slack + +import ( + "context" + "net/url" +) + +// AuthRevokeResponse contains our Auth response from the auth.revoke endpoint +type AuthRevokeResponse struct { + SlackResponse // Contains the "ok", and "Error", if any + Revoked bool `json:"revoked,omitempty"` +} + +// authRequest sends the actual request, and unmarshals the response +func (api *Client) authRequest(ctx context.Context, path string, values url.Values) (*AuthRevokeResponse, error) { + response := &AuthRevokeResponse{} + err := api.postMethod(ctx, path, values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// SendAuthRevoke will send a revocation for our token +func (api *Client) SendAuthRevoke(token string) (*AuthRevokeResponse, error) { + return api.SendAuthRevokeContext(context.Background(), token) +} + +// SendAuthRevokeContext will send a revocation request for our token to api.revoke with context +func (api *Client) SendAuthRevokeContext(ctx context.Context, token string) (*AuthRevokeResponse, error) { + if token == "" { + token = api.token + } + values := url.Values{ + "token": {token}, + } + + return api.authRequest(ctx, "auth.revoke", values) +} diff --git a/backend/vendor/github.com/slack-go/slack/block.go b/backend/vendor/github.com/slack-go/slack/block.go new file mode 100644 index 00000000..9946a294 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block.go @@ -0,0 +1,80 @@ +package slack + +// @NOTE: Blocks are in beta and subject to change. + +// More Information: https://api.slack.com/block-kit + +// MessageBlockType defines a named string type to define each block type +// as a constant for use within the package. +type MessageBlockType string + +const ( + MBTSection MessageBlockType = "section" + MBTDivider MessageBlockType = "divider" + MBTImage MessageBlockType = "image" + MBTAction MessageBlockType = "actions" + MBTContext MessageBlockType = "context" + MBTFile MessageBlockType = "file" + MBTInput MessageBlockType = "input" + MBTHeader MessageBlockType = "header" +) + +// Block defines an interface all block types should implement +// to ensure consistency between blocks. +type Block interface { + BlockType() MessageBlockType +} + +// Blocks is a convenience struct defined to allow dynamic unmarshalling of +// the "blocks" value in Slack's JSON response, which varies depending on block type +type Blocks struct { + BlockSet []Block `json:"blocks,omitempty"` +} + +// BlockAction is the action callback sent when a block is interacted with +type BlockAction struct { + ActionID string `json:"action_id"` + BlockID string `json:"block_id"` + Type actionType `json:"type"` + Text TextBlockObject `json:"text"` + Value string `json:"value"` + ActionTs string `json:"action_ts"` + SelectedOption OptionBlockObject `json:"selected_option"` + SelectedOptions []OptionBlockObject `json:"selected_options"` + SelectedUser string `json:"selected_user"` + SelectedUsers []string `json:"selected_users"` + SelectedChannel string `json:"selected_channel"` + SelectedChannels []string `json:"selected_channels"` + SelectedConversation string `json:"selected_conversation"` + SelectedConversations []string `json:"selected_conversations"` + SelectedDate string `json:"selected_date"` + SelectedTime string `json:"selected_time"` + InitialOption OptionBlockObject `json:"initial_option"` + InitialUser string `json:"initial_user"` + InitialChannel string `json:"initial_channel"` + InitialConversation string `json:"initial_conversation"` + InitialDate string `json:"initial_date"` + InitialTime string `json:"initial_time"` +} + +// actionType returns the type of the action +func (b BlockAction) actionType() actionType { + return b.Type +} + +// NewBlockMessage creates a new Message that contains one or more blocks to be displayed +func NewBlockMessage(blocks ...Block) Message { + return Message{ + Msg: Msg{ + Blocks: Blocks{ + BlockSet: blocks, + }, + }, + } +} + +// AddBlockMessage appends a block to the end of the existing list of blocks +func AddBlockMessage(message Message, newBlk Block) Message { + message.Msg.Blocks.BlockSet = append(message.Msg.Blocks.BlockSet, newBlk) + return message +} diff --git a/backend/vendor/github.com/slack-go/slack/block_action.go b/backend/vendor/github.com/slack-go/slack/block_action.go new file mode 100644 index 00000000..c15e4a3f --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block_action.go @@ -0,0 +1,26 @@ +package slack + +// ActionBlock defines data that is used to hold interactive elements. +// +// More Information: https://api.slack.com/reference/messaging/blocks#actions +type ActionBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Elements *BlockElements `json:"elements"` +} + +// BlockType returns the type of the block +func (s ActionBlock) BlockType() MessageBlockType { + return s.Type +} + +// NewActionBlock returns a new instance of an Action Block +func NewActionBlock(blockID string, elements ...BlockElement) *ActionBlock { + return &ActionBlock{ + Type: MBTAction, + BlockID: blockID, + Elements: &BlockElements{ + ElementSet: elements, + }, + } +} diff --git a/backend/vendor/github.com/slack-go/slack/block_context.go b/backend/vendor/github.com/slack-go/slack/block_context.go new file mode 100644 index 00000000..c37bf27e --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block_context.go @@ -0,0 +1,32 @@ +package slack + +// ContextBlock defines data that is used to display message context, which can +// include both images and text. +// +// More Information: https://api.slack.com/reference/messaging/blocks#actions +type ContextBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + ContextElements ContextElements `json:"elements"` +} + +// BlockType returns the type of the block +func (s ContextBlock) BlockType() MessageBlockType { + return s.Type +} + +type ContextElements struct { + Elements []MixedElement +} + +// NewContextBlock returns a new instance of a context block +func NewContextBlock(blockID string, mixedElements ...MixedElement) *ContextBlock { + elements := ContextElements{ + Elements: mixedElements, + } + return &ContextBlock{ + Type: MBTContext, + BlockID: blockID, + ContextElements: elements, + } +} diff --git a/backend/vendor/github.com/slack-go/slack/block_conv.go b/backend/vendor/github.com/slack-go/slack/block_conv.go new file mode 100644 index 00000000..f2d744c4 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block_conv.go @@ -0,0 +1,417 @@ +package slack + +import ( + "encoding/json" + "fmt" + + "github.com/pkg/errors" +) + +type sumtype struct { + TypeVal string `json:"type"` +} + +// MarshalJSON implements the Marshaller interface for Blocks so that any JSON +// marshalling is delegated and proper type determination can be made before marshal +func (b Blocks) MarshalJSON() ([]byte, error) { + bytes, err := json.Marshal(b.BlockSet) + if err != nil { + return nil, err + } + + return bytes, nil +} + +// UnmarshalJSON implements the Unmarshaller interface for Blocks, so that any JSON +// unmarshalling is delegated and proper type determination can be made before unmarshal +func (b *Blocks) UnmarshalJSON(data []byte) error { + var raw []json.RawMessage + + if string(data) == "{}" { + return nil + } + + err := json.Unmarshal(data, &raw) + if err != nil { + return err + } + + var blocks Blocks + for _, r := range raw { + s := sumtype{} + err := json.Unmarshal(r, &s) + if err != nil { + return err + } + + var blockType string + if s.TypeVal != "" { + blockType = s.TypeVal + } + + var block Block + switch blockType { + case "actions": + block = &ActionBlock{} + case "context": + block = &ContextBlock{} + case "divider": + block = &DividerBlock{} + case "file": + block = &FileBlock{} + case "header": + block = &HeaderBlock{} + case "image": + block = &ImageBlock{} + case "input": + block = &InputBlock{} + case "section": + block = &SectionBlock{} + default: + block = &UnknownBlock{} + } + + err = json.Unmarshal(r, block) + if err != nil { + return err + } + + blocks.BlockSet = append(blocks.BlockSet, block) + } + + *b = blocks + return nil +} + +// UnmarshalJSON implements the Unmarshaller interface for InputBlock, so that any JSON +// unmarshalling is delegated and proper type determination can be made before unmarshal +func (b *InputBlock) UnmarshalJSON(data []byte) error { + type alias InputBlock + a := struct { + Element json.RawMessage `json:"element"` + *alias + }{ + alias: (*alias)(b), + } + + if err := json.Unmarshal(data, &a); err != nil { + return err + } + + s := sumtype{} + if err := json.Unmarshal(a.Element, &s); err != nil { + return nil + } + + var e BlockElement + switch s.TypeVal { + case "datepicker": + e = &DatePickerBlockElement{} + case "timepicker": + e = &TimePickerBlockElement{} + case "plain_text_input": + e = &PlainTextInputBlockElement{} + case "static_select", "external_select", "users_select", "conversations_select", "channels_select": + e = &SelectBlockElement{} + case "multi_static_select", "multi_external_select", "multi_users_select", "multi_conversations_select", "multi_channels_select": + e = &MultiSelectBlockElement{} + case "checkboxes": + e = &CheckboxGroupsBlockElement{} + case "overflow": + e = &OverflowBlockElement{} + case "radio_buttons": + e = &RadioButtonsBlockElement{} + default: + return errors.New("unsupported block element type") + } + + if err := json.Unmarshal(a.Element, e); err != nil { + return err + } + b.Element = e + + return nil +} + +// MarshalJSON implements the Marshaller interface for BlockElements so that any JSON +// marshalling is delegated and proper type determination can be made before marshal +func (b *BlockElements) MarshalJSON() ([]byte, error) { + bytes, err := json.Marshal(b.ElementSet) + if err != nil { + return nil, err + } + + return bytes, nil +} + +// UnmarshalJSON implements the Unmarshaller interface for BlockElements, so that any JSON +// unmarshalling is delegated and proper type determination can be made before unmarshal +func (b *BlockElements) UnmarshalJSON(data []byte) error { + var raw []json.RawMessage + + if string(data) == "{}" { + return nil + } + + err := json.Unmarshal(data, &raw) + if err != nil { + return err + } + + var blockElements BlockElements + for _, r := range raw { + s := sumtype{} + err := json.Unmarshal(r, &s) + if err != nil { + return err + } + + var blockElementType string + if s.TypeVal != "" { + blockElementType = s.TypeVal + } + + var blockElement BlockElement + switch blockElementType { + case "image": + blockElement = &ImageBlockElement{} + case "button": + blockElement = &ButtonBlockElement{} + case "overflow": + blockElement = &OverflowBlockElement{} + case "datepicker": + blockElement = &DatePickerBlockElement{} + case "plain_text_input": + blockElement = &PlainTextInputBlockElement{} + case "checkboxes": + blockElement = &CheckboxGroupsBlockElement{} + case "radio_buttons": + blockElement = &RadioButtonsBlockElement{} + case "static_select", "external_select", "users_select", "conversations_select", "channels_select": + blockElement = &SelectBlockElement{} + default: + return fmt.Errorf("unsupported block element type %v", blockElementType) + } + + err = json.Unmarshal(r, blockElement) + if err != nil { + return err + } + + blockElements.ElementSet = append(blockElements.ElementSet, blockElement) + } + + *b = blockElements + return nil +} + +// MarshalJSON implements the Marshaller interface for Accessory so that any JSON +// marshalling is delegated and proper type determination can be made before marshal +func (a *Accessory) MarshalJSON() ([]byte, error) { + bytes, err := json.Marshal(toBlockElement(a)) + if err != nil { + return nil, err + } + + return bytes, nil +} + +// UnmarshalJSON implements the Unmarshaller interface for Accessory, so that any JSON +// unmarshalling is delegated and proper type determination can be made before unmarshal +func (a *Accessory) UnmarshalJSON(data []byte) error { + var r json.RawMessage + + if string(data) == "{\"accessory\":null}" { + return nil + } + + err := json.Unmarshal(data, &r) + if err != nil { + return err + } + + s := sumtype{} + err = json.Unmarshal(r, &s) + if err != nil { + return err + } + + var blockElementType string + if s.TypeVal != "" { + blockElementType = s.TypeVal + } + + switch blockElementType { + case "image": + element, err := unmarshalBlockElement(r, &ImageBlockElement{}) + if err != nil { + return err + } + a.ImageElement = element.(*ImageBlockElement) + case "button": + element, err := unmarshalBlockElement(r, &ButtonBlockElement{}) + if err != nil { + return err + } + a.ButtonElement = element.(*ButtonBlockElement) + case "overflow": + element, err := unmarshalBlockElement(r, &OverflowBlockElement{}) + if err != nil { + return err + } + a.OverflowElement = element.(*OverflowBlockElement) + case "datepicker": + element, err := unmarshalBlockElement(r, &DatePickerBlockElement{}) + if err != nil { + return err + } + a.DatePickerElement = element.(*DatePickerBlockElement) + case "timepicker": + element, err := unmarshalBlockElement(r, &TimePickerBlockElement{}) + if err != nil { + return err + } + a.TimePickerElement = element.(*TimePickerBlockElement) + case "plain_text_input": + element, err := unmarshalBlockElement(r, &PlainTextInputBlockElement{}) + if err != nil { + return err + } + a.PlainTextInputElement = element.(*PlainTextInputBlockElement) + case "radio_buttons": + element, err := unmarshalBlockElement(r, &RadioButtonsBlockElement{}) + if err != nil { + return err + } + a.RadioButtonsElement = element.(*RadioButtonsBlockElement) + case "static_select", "external_select", "users_select", "conversations_select", "channels_select": + element, err := unmarshalBlockElement(r, &SelectBlockElement{}) + if err != nil { + return err + } + a.SelectElement = element.(*SelectBlockElement) + case "multi_static_select", "multi_external_select", "multi_users_select", "multi_conversations_select", "multi_channels_select": + element, err := unmarshalBlockElement(r, &MultiSelectBlockElement{}) + if err != nil { + return err + } + a.MultiSelectElement = element.(*MultiSelectBlockElement) + case "checkboxes": + element, err := unmarshalBlockElement(r, &CheckboxGroupsBlockElement{}) + if err != nil { + return err + } + a.CheckboxGroupsBlockElement = element.(*CheckboxGroupsBlockElement) + default: + element, err := unmarshalBlockElement(r, &UnknownBlockElement{}) + if err != nil { + return err + } + a.UnknownElement = element.(*UnknownBlockElement) + } + + return nil +} + +func unmarshalBlockElement(r json.RawMessage, element BlockElement) (BlockElement, error) { + err := json.Unmarshal(r, element) + if err != nil { + return nil, err + } + return element, nil +} + +func toBlockElement(element *Accessory) BlockElement { + if element.ImageElement != nil { + return element.ImageElement + } + if element.ButtonElement != nil { + return element.ButtonElement + } + if element.OverflowElement != nil { + return element.OverflowElement + } + if element.DatePickerElement != nil { + return element.DatePickerElement + } + if element.TimePickerElement != nil { + return element.TimePickerElement + } + if element.PlainTextInputElement != nil { + return element.PlainTextInputElement + } + if element.RadioButtonsElement != nil { + return element.RadioButtonsElement + } + if element.CheckboxGroupsBlockElement != nil { + return element.CheckboxGroupsBlockElement + } + if element.SelectElement != nil { + return element.SelectElement + } + if element.MultiSelectElement != nil { + return element.MultiSelectElement + } + + return nil +} + +// MarshalJSON implements the Marshaller interface for ContextElements so that any JSON +// marshalling is delegated and proper type determination can be made before marshal +func (e *ContextElements) MarshalJSON() ([]byte, error) { + bytes, err := json.Marshal(e.Elements) + if err != nil { + return nil, err + } + + return bytes, nil +} + +// UnmarshalJSON implements the Unmarshaller interface for ContextElements, so that any JSON +// unmarshalling is delegated and proper type determination can be made before unmarshal +func (e *ContextElements) UnmarshalJSON(data []byte) error { + var raw []json.RawMessage + + if string(data) == "{\"elements\":null}" { + return nil + } + + err := json.Unmarshal(data, &raw) + if err != nil { + return err + } + + for _, r := range raw { + s := sumtype{} + err := json.Unmarshal(r, &s) + if err != nil { + return err + } + + var contextElementType string + if s.TypeVal != "" { + contextElementType = s.TypeVal + } + + switch contextElementType { + case PlainTextType, MarkdownType: + elem, err := unmarshalBlockObject(r, &TextBlockObject{}) + if err != nil { + return err + } + + e.Elements = append(e.Elements, elem.(*TextBlockObject)) + case "image": + elem, err := unmarshalBlockElement(r, &ImageBlockElement{}) + if err != nil { + return err + } + + e.Elements = append(e.Elements, elem.(*ImageBlockElement)) + default: + return errors.New("unsupported context element type") + } + } + + return nil +} diff --git a/backend/vendor/github.com/slack-go/slack/block_divider.go b/backend/vendor/github.com/slack-go/slack/block_divider.go new file mode 100644 index 00000000..2d442ba1 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block_divider.go @@ -0,0 +1,22 @@ +package slack + +// DividerBlock for displaying a divider line between blocks (similar to
tag in html) +// +// More Information: https://api.slack.com/reference/messaging/blocks#divider +type DividerBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` +} + +// BlockType returns the type of the block +func (s DividerBlock) BlockType() MessageBlockType { + return s.Type +} + +// NewDividerBlock returns a new instance of a divider block +func NewDividerBlock() *DividerBlock { + return &DividerBlock{ + Type: MBTDivider, + } + +} diff --git a/backend/vendor/github.com/slack-go/slack/block_element.go b/backend/vendor/github.com/slack-go/slack/block_element.go new file mode 100644 index 00000000..bf068440 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block_element.go @@ -0,0 +1,466 @@ +package slack + +// https://api.slack.com/reference/messaging/block-elements + +const ( + METCheckboxGroups MessageElementType = "checkboxes" + METImage MessageElementType = "image" + METButton MessageElementType = "button" + METOverflow MessageElementType = "overflow" + METDatepicker MessageElementType = "datepicker" + METTimepicker MessageElementType = "timepicker" + METPlainTextInput MessageElementType = "plain_text_input" + METRadioButtons MessageElementType = "radio_buttons" + + MixedElementImage MixedElementType = "mixed_image" + MixedElementText MixedElementType = "mixed_text" + + OptTypeStatic string = "static_select" + OptTypeExternal string = "external_select" + OptTypeUser string = "users_select" + OptTypeConversations string = "conversations_select" + OptTypeChannels string = "channels_select" + + MultiOptTypeStatic string = "multi_static_select" + MultiOptTypeExternal string = "multi_external_select" + MultiOptTypeUser string = "multi_users_select" + MultiOptTypeConversations string = "multi_conversations_select" + MultiOptTypeChannels string = "multi_channels_select" +) + +type MessageElementType string +type MixedElementType string + +// BlockElement defines an interface that all block element types should implement. +type BlockElement interface { + ElementType() MessageElementType +} + +type MixedElement interface { + MixedElementType() MixedElementType +} + +type Accessory struct { + ImageElement *ImageBlockElement + ButtonElement *ButtonBlockElement + OverflowElement *OverflowBlockElement + DatePickerElement *DatePickerBlockElement + TimePickerElement *TimePickerBlockElement + PlainTextInputElement *PlainTextInputBlockElement + RadioButtonsElement *RadioButtonsBlockElement + SelectElement *SelectBlockElement + MultiSelectElement *MultiSelectBlockElement + CheckboxGroupsBlockElement *CheckboxGroupsBlockElement + UnknownElement *UnknownBlockElement +} + +// NewAccessory returns a new Accessory for a given block element +func NewAccessory(element BlockElement) *Accessory { + switch element.(type) { + case *ImageBlockElement: + return &Accessory{ImageElement: element.(*ImageBlockElement)} + case *ButtonBlockElement: + return &Accessory{ButtonElement: element.(*ButtonBlockElement)} + case *OverflowBlockElement: + return &Accessory{OverflowElement: element.(*OverflowBlockElement)} + case *DatePickerBlockElement: + return &Accessory{DatePickerElement: element.(*DatePickerBlockElement)} + case *TimePickerBlockElement: + return &Accessory{TimePickerElement: element.(*TimePickerBlockElement)} + case *PlainTextInputBlockElement: + return &Accessory{PlainTextInputElement: element.(*PlainTextInputBlockElement)} + case *RadioButtonsBlockElement: + return &Accessory{RadioButtonsElement: element.(*RadioButtonsBlockElement)} + case *SelectBlockElement: + return &Accessory{SelectElement: element.(*SelectBlockElement)} + case *MultiSelectBlockElement: + return &Accessory{MultiSelectElement: element.(*MultiSelectBlockElement)} + case *CheckboxGroupsBlockElement: + return &Accessory{CheckboxGroupsBlockElement: element.(*CheckboxGroupsBlockElement)} + default: + return &Accessory{UnknownElement: element.(*UnknownBlockElement)} + } +} + +// BlockElements is a convenience struct defined to allow dynamic unmarshalling of +// the "elements" value in Slack's JSON response, which varies depending on BlockElement type +type BlockElements struct { + ElementSet []BlockElement `json:"elements,omitempty"` +} + +// UnknownBlockElement any block element that this library does not directly support. +// See the "Rich Elements" section at the following URL: +// https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less +// New block element types may be introduced by Slack at any time; this is a catch-all for any such block elements. +type UnknownBlockElement struct { + Type MessageElementType `json:"type"` + Elements BlockElements +} + +// ElementType returns the type of the Element +func (s UnknownBlockElement) ElementType() MessageElementType { + return s.Type +} + +// ImageBlockElement An element to insert an image - this element can be used +// in section and context blocks only. If you want a block with only an image +// in it, you're looking for the image block. +// +// More Information: https://api.slack.com/reference/messaging/block-elements#image +type ImageBlockElement struct { + Type MessageElementType `json:"type"` + ImageURL string `json:"image_url"` + AltText string `json:"alt_text"` +} + +// ElementType returns the type of the Element +func (s ImageBlockElement) ElementType() MessageElementType { + return s.Type +} + +func (s ImageBlockElement) MixedElementType() MixedElementType { + return MixedElementImage +} + +// NewImageBlockElement returns a new instance of an image block element +func NewImageBlockElement(imageURL, altText string) *ImageBlockElement { + return &ImageBlockElement{ + Type: METImage, + ImageURL: imageURL, + AltText: altText, + } +} + +// Style is a style of Button element +// https://api.slack.com/reference/block-kit/block-elements#button__fields +type Style string + +const ( + StyleDefault Style = "" + StylePrimary Style = "primary" + StyleDanger Style = "danger" +) + +// ButtonBlockElement defines an interactive element that inserts a button. The +// button can be a trigger for anything from opening a simple link to starting +// a complex workflow. +// +// More Information: https://api.slack.com/reference/messaging/block-elements#button +type ButtonBlockElement struct { + Type MessageElementType `json:"type,omitempty"` + Text *TextBlockObject `json:"text"` + ActionID string `json:"action_id,omitempty"` + URL string `json:"url,omitempty"` + Value string `json:"value,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + Style Style `json:"style,omitempty"` +} + +// ElementType returns the type of the element +func (s ButtonBlockElement) ElementType() MessageElementType { + return s.Type +} + +// WithStyle adds styling to the button object and returns the modified ButtonBlockElement +func (s *ButtonBlockElement) WithStyle(style Style) *ButtonBlockElement { + s.Style = style + return s +} + +// NewButtonBlockElement returns an instance of a new button element to be used within a block +func NewButtonBlockElement(actionID, value string, text *TextBlockObject) *ButtonBlockElement { + return &ButtonBlockElement{ + Type: METButton, + ActionID: actionID, + Text: text, + Value: value, + } +} + +// OptionsResponse defines the response used for select block typahead. +// +// More Information: https://api.slack.com/reference/block-kit/block-elements#external_multi_select +type OptionsResponse struct { + Options []*OptionBlockObject `json:"options,omitempty"` +} + +// OptionGroupsResponse defines the response used for select block typahead. +// +// More Information: https://api.slack.com/reference/block-kit/block-elements#external_multi_select +type OptionGroupsResponse struct { + OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"` +} + +// SelectBlockElement defines the simplest form of select menu, with a static list +// of options passed in when defining the element. +// +// More Information: https://api.slack.com/reference/messaging/block-elements#select +type SelectBlockElement struct { + Type string `json:"type,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + ActionID string `json:"action_id,omitempty"` + Options []*OptionBlockObject `json:"options,omitempty"` + OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"` + InitialOption *OptionBlockObject `json:"initial_option,omitempty"` + InitialUser string `json:"initial_user,omitempty"` + InitialConversation string `json:"initial_conversation,omitempty"` + InitialChannel string `json:"initial_channel,omitempty"` + DefaultToCurrentConversation bool `json:"default_to_current_conversation,omitempty"` + ResponseURLEnabled bool `json:"response_url_enabled,omitempty"` + Filter *SelectBlockElementFilter `json:"filter,omitempty"` + MinQueryLength *int `json:"min_query_length,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` +} + +// SelectBlockElementFilter allows to filter select element conversation options by type. +// +// More Information: https://api.slack.com/reference/block-kit/composition-objects#filter_conversations +type SelectBlockElementFilter struct { + Include []string `json:"include,omitempty"` + ExcludeExternalSharedChannels bool `json:"exclude_external_shared_channels,omitempty"` + ExcludeBotUsers bool `json:"exclude_bot_users,omitempty"` +} + +// ElementType returns the type of the Element +func (s SelectBlockElement) ElementType() MessageElementType { + return MessageElementType(s.Type) +} + +// NewOptionsSelectBlockElement returns a new instance of SelectBlockElement for use with +// the Options object only. +func NewOptionsSelectBlockElement(optType string, placeholder *TextBlockObject, actionID string, options ...*OptionBlockObject) *SelectBlockElement { + return &SelectBlockElement{ + Type: optType, + Placeholder: placeholder, + ActionID: actionID, + Options: options, + } +} + +// NewOptionsGroupSelectBlockElement returns a new instance of SelectBlockElement for use with +// the Options object only. +func NewOptionsGroupSelectBlockElement( + optType string, + placeholder *TextBlockObject, + actionID string, + optGroups ...*OptionGroupBlockObject, +) *SelectBlockElement { + return &SelectBlockElement{ + Type: optType, + Placeholder: placeholder, + ActionID: actionID, + OptionGroups: optGroups, + } +} + +// MultiSelectBlockElement defines a multiselect menu, with a static list +// of options passed in when defining the element. +// +// More Information: https://api.slack.com/reference/messaging/block-elements#multi_select +type MultiSelectBlockElement struct { + Type string `json:"type,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + ActionID string `json:"action_id,omitempty"` + Options []*OptionBlockObject `json:"options,omitempty"` + OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"` + InitialOptions []*OptionBlockObject `json:"initial_options,omitempty"` + InitialUsers []string `json:"initial_users,omitempty"` + InitialConversations []string `json:"initial_conversations,omitempty"` + InitialChannels []string `json:"initial_channels,omitempty"` + MinQueryLength *int `json:"min_query_length,omitempty"` + MaxSelectedItems *int `json:"max_selected_items,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` +} + +// ElementType returns the type of the Element +func (s MultiSelectBlockElement) ElementType() MessageElementType { + return MessageElementType(s.Type) +} + +// NewOptionsMultiSelectBlockElement returns a new instance of SelectBlockElement for use with +// the Options object only. +func NewOptionsMultiSelectBlockElement(optType string, placeholder *TextBlockObject, actionID string, options ...*OptionBlockObject) *MultiSelectBlockElement { + return &MultiSelectBlockElement{ + Type: optType, + Placeholder: placeholder, + ActionID: actionID, + Options: options, + } +} + +// NewOptionsGroupMultiSelectBlockElement returns a new instance of MultiSelectBlockElement for use with +// the Options object only. +func NewOptionsGroupMultiSelectBlockElement( + optType string, + placeholder *TextBlockObject, + actionID string, + optGroups ...*OptionGroupBlockObject, +) *MultiSelectBlockElement { + return &MultiSelectBlockElement{ + Type: optType, + Placeholder: placeholder, + ActionID: actionID, + OptionGroups: optGroups, + } +} + +// OverflowBlockElement defines the fields needed to use an overflow element. +// And Overflow Element is like a cross between a button and a select menu - +// when a user clicks on this overflow button, they will be presented with a +// list of options to choose from. +// +// More Information: https://api.slack.com/reference/messaging/block-elements#overflow +type OverflowBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Options []*OptionBlockObject `json:"options"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` +} + +// ElementType returns the type of the Element +func (s OverflowBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewOverflowBlockElement returns an instance of a new Overflow Block Element +func NewOverflowBlockElement(actionID string, options ...*OptionBlockObject) *OverflowBlockElement { + return &OverflowBlockElement{ + Type: METOverflow, + ActionID: actionID, + Options: options, + } +} + +// DatePickerBlockElement defines an element which lets users easily select a +// date from a calendar style UI. Date picker elements can be used inside of +// section and actions blocks. +// +// More Information: https://api.slack.com/reference/messaging/block-elements#datepicker +type DatePickerBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + InitialDate string `json:"initial_date,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` +} + +// ElementType returns the type of the Element +func (s DatePickerBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewDatePickerBlockElement returns an instance of a date picker element +func NewDatePickerBlockElement(actionID string) *DatePickerBlockElement { + return &DatePickerBlockElement{ + Type: METDatepicker, + ActionID: actionID, + } +} + +// TimePickerBlockElement defines an element which lets users easily select a +// time from nice UI. Time picker elements can be used inside of +// section and actions blocks. +// +// More Information: https://api.slack.com/reference/messaging/block-elements#timepicker +type TimePickerBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + InitialTime string `json:"initial_time,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` +} + +// ElementType returns the type of the Element +func (s TimePickerBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewTimePickerBlockElement returns an instance of a date picker element +func NewTimePickerBlockElement(actionID string) *TimePickerBlockElement { + return &TimePickerBlockElement{ + Type: METTimepicker, + ActionID: actionID, + } +} + +// PlainTextInputBlockElement creates a field where a user can enter freeform +// data. +// Plain-text input elements are currently only available in modals. +// +// More Information: https://api.slack.com/reference/block-kit/block-elements#input +type PlainTextInputBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + InitialValue string `json:"initial_value,omitempty"` + Multiline bool `json:"multiline,omitempty"` + MinLength int `json:"min_length,omitempty"` + MaxLength int `json:"max_length,omitempty"` +} + +// ElementType returns the type of the Element +func (s PlainTextInputBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewPlainTextInputBlockElement returns an instance of a plain-text input +// element +func NewPlainTextInputBlockElement(placeholder *TextBlockObject, actionID string) *PlainTextInputBlockElement { + return &PlainTextInputBlockElement{ + Type: METPlainTextInput, + ActionID: actionID, + Placeholder: placeholder, + } +} + +// CheckboxGroupsBlockElement defines an element which allows users to choose +// one or more items from a list of possible options. +// +// More Information: https://api.slack.com/reference/block-kit/block-elements#checkboxes +type CheckboxGroupsBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Options []*OptionBlockObject `json:"options"` + InitialOptions []*OptionBlockObject `json:"initial_options,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` +} + +// ElementType returns the type of the Element +func (c CheckboxGroupsBlockElement) ElementType() MessageElementType { + return c.Type +} + +// NewCheckboxGroupsBlockElement returns an instance of a checkbox-group block element +func NewCheckboxGroupsBlockElement(actionID string, options ...*OptionBlockObject) *CheckboxGroupsBlockElement { + return &CheckboxGroupsBlockElement{ + Type: METCheckboxGroups, + ActionID: actionID, + Options: options, + } +} + +// RadioButtonsBlockElement defines an element which lets users choose one item +// from a list of possible options. +// +// More Information: https://api.slack.com/reference/block-kit/block-elements#radio +type RadioButtonsBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Options []*OptionBlockObject `json:"options"` + InitialOption *OptionBlockObject `json:"initial_option,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` +} + +// ElementType returns the type of the Element +func (s RadioButtonsBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewRadioButtonsBlockElement returns an instance of a radio buttons element. +func NewRadioButtonsBlockElement(actionID string, options ...*OptionBlockObject) *RadioButtonsBlockElement { + return &RadioButtonsBlockElement{ + Type: METRadioButtons, + ActionID: actionID, + Options: options, + } +} diff --git a/backend/vendor/github.com/slack-go/slack/block_file.go b/backend/vendor/github.com/slack-go/slack/block_file.go new file mode 100644 index 00000000..ac4453f7 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block_file.go @@ -0,0 +1,26 @@ +package slack + +// FileBlock defines data that is used to display a remote file. +// +// More Information: https://api.slack.com/reference/block-kit/blocks#file +type FileBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + ExternalID string `json:"external_id"` + Source string `json:"source"` +} + +// BlockType returns the type of the block +func (s FileBlock) BlockType() MessageBlockType { + return s.Type +} + +// NewFileBlock returns a new instance of a file block +func NewFileBlock(blockID string, externalID string, source string) *FileBlock { + return &FileBlock{ + Type: MBTFile, + BlockID: blockID, + ExternalID: externalID, + Source: source, + } +} diff --git a/backend/vendor/github.com/slack-go/slack/block_header.go b/backend/vendor/github.com/slack-go/slack/block_header.go new file mode 100644 index 00000000..6dff4b88 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block_header.go @@ -0,0 +1,38 @@ +package slack + +// HeaderBlock defines a new block of type header +// +// More Information: https://api.slack.com/reference/messaging/blocks#header +type HeaderBlock struct { + Type MessageBlockType `json:"type"` + Text *TextBlockObject `json:"text,omitempty"` + BlockID string `json:"block_id,omitempty"` +} + +// BlockType returns the type of the block +func (s HeaderBlock) BlockType() MessageBlockType { + return s.Type +} + +// HeaderBlockOption allows configuration of options for a new header block +type HeaderBlockOption func(*HeaderBlock) + +func HeaderBlockOptionBlockID(blockID string) HeaderBlockOption { + return func(block *HeaderBlock) { + block.BlockID = blockID + } +} + +// NewHeaderBlock returns a new instance of a header block to be rendered +func NewHeaderBlock(textObj *TextBlockObject, options ...HeaderBlockOption) *HeaderBlock { + block := HeaderBlock{ + Type: MBTHeader, + Text: textObj, + } + + for _, option := range options { + option(&block) + } + + return &block +} diff --git a/backend/vendor/github.com/slack-go/slack/block_image.go b/backend/vendor/github.com/slack-go/slack/block_image.go new file mode 100644 index 00000000..90cbd14e --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block_image.go @@ -0,0 +1,28 @@ +package slack + +// ImageBlock defines data required to display an image as a block element +// +// More Information: https://api.slack.com/reference/messaging/blocks#image +type ImageBlock struct { + Type MessageBlockType `json:"type"` + ImageURL string `json:"image_url"` + AltText string `json:"alt_text"` + BlockID string `json:"block_id,omitempty"` + Title *TextBlockObject `json:"title,omitempty"` +} + +// BlockType returns the type of the block +func (s ImageBlock) BlockType() MessageBlockType { + return s.Type +} + +// NewImageBlock returns an instance of a new Image Block type +func NewImageBlock(imageURL, altText, blockID string, title *TextBlockObject) *ImageBlock { + return &ImageBlock{ + Type: MBTImage, + ImageURL: imageURL, + AltText: altText, + BlockID: blockID, + Title: title, + } +} diff --git a/backend/vendor/github.com/slack-go/slack/block_input.go b/backend/vendor/github.com/slack-go/slack/block_input.go new file mode 100644 index 00000000..087571af --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block_input.go @@ -0,0 +1,29 @@ +package slack + +// InputBlock defines data that is used to display user input fields. +// +// More Information: https://api.slack.com/reference/block-kit/blocks#input +type InputBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Label *TextBlockObject `json:"label"` + Element BlockElement `json:"element"` + Hint *TextBlockObject `json:"hint,omitempty"` + Optional bool `json:"optional,omitempty"` + DispatchAction bool `json:"dispatch_action,omitempty"` +} + +// BlockType returns the type of the block +func (s InputBlock) BlockType() MessageBlockType { + return s.Type +} + +// NewInputBlock returns a new instance of an input block +func NewInputBlock(blockID string, label *TextBlockObject, element BlockElement) *InputBlock { + return &InputBlock{ + Type: MBTInput, + BlockID: blockID, + Label: label, + Element: element, + } +} diff --git a/backend/vendor/github.com/slack-go/slack/block_object.go b/backend/vendor/github.com/slack-go/slack/block_object.go new file mode 100644 index 00000000..5ced7f92 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block_object.go @@ -0,0 +1,247 @@ +package slack + +import ( + "encoding/json" + "errors" +) + +// Block Objects are also known as Composition Objects +// +// For more information: https://api.slack.com/reference/messaging/composition-objects + +// BlockObject defines an interface that all block object types should +// implement. +// @TODO: Is this interface needed? + +// blockObject object types +const ( + MarkdownType = "mrkdwn" + PlainTextType = "plain_text" + // The following objects don't actually have types and their corresponding + // const values are just for internal use + motConfirmation = "confirm" + motOption = "option" + motOptionGroup = "option_group" +) + +type MessageObjectType string + +type blockObject interface { + validateType() MessageObjectType +} + +type BlockObjects struct { + TextObjects []*TextBlockObject + ConfirmationObjects []*ConfirmationBlockObject + OptionObjects []*OptionBlockObject + OptionGroupObjects []*OptionGroupBlockObject +} + +// UnmarshalJSON implements the Unmarshaller interface for BlockObjects, so that any JSON +// unmarshalling is delegated and proper type determination can be made before unmarshal +func (b *BlockObjects) UnmarshalJSON(data []byte) error { + var raw []json.RawMessage + err := json.Unmarshal(data, &raw) + if err != nil { + return err + } + + for _, r := range raw { + var obj map[string]interface{} + err := json.Unmarshal(r, &obj) + if err != nil { + return err + } + + blockObjectType := getBlockObjectType(obj) + + switch blockObjectType { + case PlainTextType, MarkdownType: + object, err := unmarshalBlockObject(r, &TextBlockObject{}) + if err != nil { + return err + } + b.TextObjects = append(b.TextObjects, object.(*TextBlockObject)) + case motConfirmation: + object, err := unmarshalBlockObject(r, &ConfirmationBlockObject{}) + if err != nil { + return err + } + b.ConfirmationObjects = append(b.ConfirmationObjects, object.(*ConfirmationBlockObject)) + case motOption: + object, err := unmarshalBlockObject(r, &OptionBlockObject{}) + if err != nil { + return err + } + b.OptionObjects = append(b.OptionObjects, object.(*OptionBlockObject)) + case motOptionGroup: + object, err := unmarshalBlockObject(r, &OptionGroupBlockObject{}) + if err != nil { + return err + } + b.OptionGroupObjects = append(b.OptionGroupObjects, object.(*OptionGroupBlockObject)) + + } + } + + return nil +} + +// Ideally would have a better way to identify the block objects for +// type casting at time of unmarshalling, should be adapted if possible +// to accomplish in a more reliable manner. +func getBlockObjectType(obj map[string]interface{}) string { + if t, ok := obj["type"].(string); ok { + return t + } + if _, ok := obj["confirm"].(string); ok { + return "confirm" + } + if _, ok := obj["options"].(string); ok { + return "option_group" + } + if _, ok := obj["text"].(string); ok { + if _, ok := obj["value"].(string); ok { + return "option" + } + } + return "" +} + +func unmarshalBlockObject(r json.RawMessage, object blockObject) (blockObject, error) { + err := json.Unmarshal(r, object) + if err != nil { + return nil, err + } + return object, nil +} + +// TextBlockObject defines a text element object to be used with blocks +// +// More Information: https://api.slack.com/reference/messaging/composition-objects#text +type TextBlockObject struct { + Type string `json:"type"` + Text string `json:"text"` + Emoji bool `json:"emoji,omitempty"` + Verbatim bool `json:"verbatim,omitempty"` +} + +// validateType enforces block objects for element and block parameters +func (s TextBlockObject) validateType() MessageObjectType { + return MessageObjectType(s.Type) +} + +// validateType enforces block objects for element and block parameters +func (s TextBlockObject) MixedElementType() MixedElementType { + return MixedElementText +} + +// Validate checks if TextBlockObject has valid values +func (s TextBlockObject) Validate() error { + if s.Type != "plain_text" && s.Type != "mrkdwn" { + return errors.New("type must be either of plain_text or mrkdwn") + } + + // https://github.com/slack-go/slack/issues/881 + if s.Type == "mrkdwn" && s.Emoji { + return errors.New("emoji cannot be true in mrkdown") + } + + return nil +} + +// NewTextBlockObject returns an instance of a new Text Block Object +func NewTextBlockObject(elementType, text string, emoji, verbatim bool) *TextBlockObject { + return &TextBlockObject{ + Type: elementType, + Text: text, + Emoji: emoji, + Verbatim: verbatim, + } +} + +// BlockType returns the type of the block +func (t TextBlockObject) BlockType() MessageBlockType { + if t.Type == "mrkdwn" { + return MarkdownType + } + return PlainTextType +} + +// ConfirmationBlockObject defines a dialog that provides a confirmation step to +// any interactive element. This dialog will ask the user to confirm their action by +// offering a confirm and deny buttons. +// +// More Information: https://api.slack.com/reference/messaging/composition-objects#confirm +type ConfirmationBlockObject struct { + Title *TextBlockObject `json:"title"` + Text *TextBlockObject `json:"text"` + Confirm *TextBlockObject `json:"confirm"` + Deny *TextBlockObject `json:"deny"` + Style Style `json:"style,omitempty"` +} + +// validateType enforces block objects for element and block parameters +func (s ConfirmationBlockObject) validateType() MessageObjectType { + return motConfirmation +} + +// WithStyle add styling to confirmation object +func (s *ConfirmationBlockObject) WithStyle(style Style) { + s.Style = style +} + +// NewConfirmationBlockObject returns an instance of a new Confirmation Block Object +func NewConfirmationBlockObject(title, text, confirm, deny *TextBlockObject) *ConfirmationBlockObject { + return &ConfirmationBlockObject{ + Title: title, + Text: text, + Confirm: confirm, + Deny: deny, + } +} + +// OptionBlockObject represents a single selectable item in a select menu +// +// More Information: https://api.slack.com/reference/messaging/composition-objects#option +type OptionBlockObject struct { + Text *TextBlockObject `json:"text"` + Value string `json:"value"` + Description *TextBlockObject `json:"description,omitempty"` + URL string `json:"url,omitempty"` +} + +// NewOptionBlockObject returns an instance of a new Option Block Element +func NewOptionBlockObject(value string, text, description *TextBlockObject) *OptionBlockObject { + return &OptionBlockObject{ + Text: text, + Value: value, + Description: description, + } +} + +// validateType enforces block objects for element and block parameters +func (s OptionBlockObject) validateType() MessageObjectType { + return motOption +} + +// OptionGroupBlockObject Provides a way to group options in a select menu. +// +// More Information: https://api.slack.com/reference/messaging/composition-objects#option-group +type OptionGroupBlockObject struct { + Label *TextBlockObject `json:"label,omitempty"` + Options []*OptionBlockObject `json:"options"` +} + +// validateType enforces block objects for element and block parameters +func (s OptionGroupBlockObject) validateType() MessageObjectType { + return motOptionGroup +} + +// NewOptionGroupBlockElement returns an instance of a new option group block element +func NewOptionGroupBlockElement(label *TextBlockObject, options ...*OptionBlockObject) *OptionGroupBlockObject { + return &OptionGroupBlockObject{ + Label: label, + Options: options, + } +} diff --git a/backend/vendor/github.com/slack-go/slack/block_section.go b/backend/vendor/github.com/slack-go/slack/block_section.go new file mode 100644 index 00000000..01ffd5a1 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block_section.go @@ -0,0 +1,42 @@ +package slack + +// SectionBlock defines a new block of type section +// +// More Information: https://api.slack.com/reference/messaging/blocks#section +type SectionBlock struct { + Type MessageBlockType `json:"type"` + Text *TextBlockObject `json:"text,omitempty"` + BlockID string `json:"block_id,omitempty"` + Fields []*TextBlockObject `json:"fields,omitempty"` + Accessory *Accessory `json:"accessory,omitempty"` +} + +// BlockType returns the type of the block +func (s SectionBlock) BlockType() MessageBlockType { + return s.Type +} + +// SectionBlockOption allows configuration of options for a new section block +type SectionBlockOption func(*SectionBlock) + +func SectionBlockOptionBlockID(blockID string) SectionBlockOption { + return func(block *SectionBlock) { + block.BlockID = blockID + } +} + +// NewSectionBlock returns a new instance of a section block to be rendered +func NewSectionBlock(textObj *TextBlockObject, fields []*TextBlockObject, accessory *Accessory, options ...SectionBlockOption) *SectionBlock { + block := SectionBlock{ + Type: MBTSection, + Text: textObj, + Fields: fields, + Accessory: accessory, + } + + for _, option := range options { + option(&block) + } + + return &block +} diff --git a/backend/vendor/github.com/slack-go/slack/block_unknown.go b/backend/vendor/github.com/slack-go/slack/block_unknown.go new file mode 100644 index 00000000..97054c73 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/block_unknown.go @@ -0,0 +1,13 @@ +package slack + +// UnknownBlock represents a block type that is not yet known. This block type exists to prevent Slack from introducing +// new and unknown block types that break this library. +type UnknownBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` +} + +// BlockType returns the type of the block +func (b UnknownBlock) BlockType() MessageBlockType { + return b.Type +} diff --git a/backend/vendor/github.com/slack-go/slack/bots.go b/backend/vendor/github.com/slack-go/slack/bots.go new file mode 100644 index 00000000..da21ba0c --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/bots.go @@ -0,0 +1,58 @@ +package slack + +import ( + "context" + "net/url" +) + +// Bot contains information about a bot +type Bot struct { + ID string `json:"id"` + Name string `json:"name"` + Deleted bool `json:"deleted"` + UserID string `json:"user_id"` + AppID string `json:"app_id"` + Updated JSONTime `json:"updated"` + Icons Icons `json:"icons"` +} + +type botResponseFull struct { + Bot `json:"bot,omitempty"` // GetBotInfo + SlackResponse +} + +func (api *Client) botRequest(ctx context.Context, path string, values url.Values) (*botResponseFull, error) { + response := &botResponseFull{} + err := api.postMethod(ctx, path, values, response) + if err != nil { + return nil, err + } + + if err := response.Err(); err != nil { + return nil, err + } + + return response, nil +} + +// GetBotInfo will retrieve the complete bot information +func (api *Client) GetBotInfo(bot string) (*Bot, error) { + return api.GetBotInfoContext(context.Background(), bot) +} + +// GetBotInfoContext will retrieve the complete bot information using a custom context +func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) { + values := url.Values{ + "token": {api.token}, + } + + if bot != "" { + values.Add("bot", bot) + } + + response, err := api.botRequest(ctx, "bots.info", values) + if err != nil { + return nil, err + } + return &response.Bot, nil +} diff --git a/backend/vendor/github.com/slack-go/slack/channels.go b/backend/vendor/github.com/slack-go/slack/channels.go new file mode 100644 index 00000000..c05521d0 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/channels.go @@ -0,0 +1,714 @@ +package slack + +import ( + "context" + "net/url" + "strconv" + "time" +) + +type channelResponseFull struct { + Channel Channel `json:"channel"` + Channels []Channel `json:"channels"` + Purpose string `json:"purpose"` + Topic string `json:"topic"` + NotInChannel bool `json:"not_in_channel"` + History + SlackResponse + Metadata ResponseMetadata `json:"response_metadata"` +} + +// Channel contains information about the channel +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +type Channel struct { + GroupConversation + IsChannel bool `json:"is_channel"` + IsGeneral bool `json:"is_general"` + IsMember bool `json:"is_member"` + Locale string `json:"locale"` +} + +func (api *Client) channelRequest(ctx context.Context, path string, values url.Values) (*channelResponseFull, error) { + response := &channelResponseFull{} + err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// GetChannelsOption option provided when getting channels. +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +type GetChannelsOption func(*ChannelPagination) error + +// GetChannelsOptionExcludeMembers excludes the members collection from each channel. +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func GetChannelsOptionExcludeMembers() GetChannelsOption { + return func(p *ChannelPagination) error { + p.excludeMembers = true + return nil + } +} + +// GetChannelsOptionExcludeArchived excludes archived channels from results. +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func GetChannelsOptionExcludeArchived() GetChannelsOption { + return func(p *ChannelPagination) error { + p.excludeArchived = true + return nil + } +} + +// ArchiveChannel archives the given channel +// see https://api.slack.com/methods/channels.archive +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) ArchiveChannel(channelID string) error { + return api.ArchiveChannelContext(context.Background(), channelID) +} + +// ArchiveChannelContext archives the given channel with a custom context +// see https://api.slack.com/methods/channels.archive +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) (err error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + + _, err = api.channelRequest(ctx, "channels.archive", values) + return err +} + +// UnarchiveChannel unarchives the given channel +// see https://api.slack.com/methods/channels.unarchive +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) UnarchiveChannel(channelID string) error { + return api.UnarchiveChannelContext(context.Background(), channelID) +} + +// UnarchiveChannelContext unarchives the given channel with a custom context +// see https://api.slack.com/methods/channels.unarchive +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string) (err error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + + _, err = api.channelRequest(ctx, "channels.unarchive", values) + return err +} + +// CreateChannel creates a channel with the given name and returns a *Channel +// see https://api.slack.com/methods/channels.create +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) CreateChannel(channelName string) (*Channel, error) { + return api.CreateChannelContext(context.Background(), channelName) +} + +// CreateChannelContext creates a channel with the given name and returns a *Channel with a custom context +// see https://api.slack.com/methods/channels.create +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) CreateChannelContext(ctx context.Context, channelName string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "name": {channelName}, + } + + response, err := api.channelRequest(ctx, "channels.create", values) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// GetChannelHistory retrieves the channel history +// see https://api.slack.com/methods/channels.history +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetChannelHistory(channelID string, params HistoryParameters) (*History, error) { + return api.GetChannelHistoryContext(context.Background(), channelID, params) +} + +// GetChannelHistoryContext retrieves the channel history with a custom context +// see https://api.slack.com/methods/channels.history +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID string, params HistoryParameters) (*History, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + if params.Latest != DEFAULT_HISTORY_LATEST { + values.Add("latest", params.Latest) + } + if params.Oldest != DEFAULT_HISTORY_OLDEST { + values.Add("oldest", params.Oldest) + } + if params.Count != DEFAULT_HISTORY_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + } + + if params.Unreads != DEFAULT_HISTORY_UNREADS { + if params.Unreads { + values.Add("unreads", "1") + } else { + values.Add("unreads", "0") + } + } + + response, err := api.channelRequest(ctx, "channels.history", values) + if err != nil { + return nil, err + } + return &response.History, nil +} + +// GetChannelInfo retrieves the given channel +// see https://api.slack.com/methods/channels.info +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetChannelInfo(channelID string) (*Channel, error) { + return api.GetChannelInfoContext(context.Background(), channelID) +} + +// GetChannelInfoContext retrieves the given channel with a custom context +// see https://api.slack.com/methods/channels.info +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "include_locale": {strconv.FormatBool(true)}, + } + + response, err := api.channelRequest(ctx, "channels.info", values) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// InviteUserToChannel invites a user to a given channel and returns a *Channel +// see https://api.slack.com/methods/channels.invite +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error) { + return api.InviteUserToChannelContext(context.Background(), channelID, user) +} + +// InviteUserToChannelContext invites a user to a given channel and returns a *Channel with a custom context +// see https://api.slack.com/methods/channels.invite +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "user": {user}, + } + + response, err := api.channelRequest(ctx, "channels.invite", values) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// JoinChannel joins the currently authenticated user to a channel +// see https://api.slack.com/methods/channels.join +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) JoinChannel(channelName string) (*Channel, error) { + return api.JoinChannelContext(context.Background(), channelName) +} + +// JoinChannelContext joins the currently authenticated user to a channel with a custom context +// see https://api.slack.com/methods/channels.join +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "name": {channelName}, + } + + response, err := api.channelRequest(ctx, "channels.join", values) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// LeaveChannel makes the authenticated user leave the given channel +// see https://api.slack.com/methods/channels.leave +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) LeaveChannel(channelID string) (bool, error) { + return api.LeaveChannelContext(context.Background(), channelID) +} + +// LeaveChannelContext makes the authenticated user leave the given channel with a custom context +// see https://api.slack.com/methods/channels.leave +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (bool, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + + response, err := api.channelRequest(ctx, "channels.leave", values) + if err != nil { + return false, err + } + + return response.NotInChannel, nil +} + +// KickUserFromChannel kicks a user from a given channel +// see https://api.slack.com/methods/channels.kick +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) KickUserFromChannel(channelID, user string) error { + return api.KickUserFromChannelContext(context.Background(), channelID, user) +} + +// KickUserFromChannelContext kicks a user from a given channel with a custom context +// see https://api.slack.com/methods/channels.kick +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, user string) (err error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "user": {user}, + } + + _, err = api.channelRequest(ctx, "channels.kick", values) + return err +} + +func newChannelPagination(c *Client, options ...GetChannelsOption) (cp ChannelPagination) { + cp = ChannelPagination{ + c: c, + limit: 200, // per slack api documentation. + } + + for _, opt := range options { + opt(&cp) + } + + return cp +} + +// ChannelPagination allows for paginating over the channels +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +type ChannelPagination struct { + Channels []Channel + limit int + excludeArchived bool + excludeMembers bool + previousResp *ResponseMetadata + c *Client +} + +// Done checks if the pagination has completed +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (ChannelPagination) Done(err error) bool { + return err == errPaginationComplete +} + +// Failure checks if pagination failed. +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (t ChannelPagination) Failure(err error) error { + if t.Done(err) { + return nil + } + + return err +} + +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (t ChannelPagination) Next(ctx context.Context) (_ ChannelPagination, err error) { + var ( + resp *channelResponseFull + ) + + if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") { + return t, errPaginationComplete + } + + t.previousResp = t.previousResp.initialize() + + values := url.Values{ + "limit": {strconv.Itoa(t.limit)}, + "exclude_archived": {strconv.FormatBool(t.excludeArchived)}, + "exclude_members": {strconv.FormatBool(t.excludeMembers)}, + "token": {t.c.token}, + "cursor": {t.previousResp.Cursor}, + } + + if resp, err = t.c.channelRequest(ctx, "channels.list", values); err != nil { + return t, err + } + + t.c.Debugf("GetChannelsContext: got %d channels; metadata %v", len(resp.Channels), resp.Metadata) + t.Channels = resp.Channels + t.previousResp = &resp.Metadata + + return t, nil +} + +// GetChannelsPaginated fetches channels in a paginated fashion, see GetChannelsContext for usage. +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetChannelsPaginated(options ...GetChannelsOption) ChannelPagination { + return newChannelPagination(api, options...) +} + +// GetChannels retrieves all the channels +// see https://api.slack.com/methods/channels.list +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetChannels(excludeArchived bool, options ...GetChannelsOption) ([]Channel, error) { + return api.GetChannelsContext(context.Background(), excludeArchived, options...) +} + +// GetChannelsContext retrieves all the channels with a custom context +// see https://api.slack.com/methods/channels.list +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool, options ...GetChannelsOption) (results []Channel, err error) { + if excludeArchived { + options = append(options, GetChannelsOptionExcludeArchived()) + } + + p := api.GetChannelsPaginated(options...) + for err == nil { + p, err = p.Next(ctx) + if err == nil { + results = append(results, p.Channels...) + } else if rateLimitedError, ok := err.(*RateLimitedError); ok { + select { + case <-ctx.Done(): + err = ctx.Err() + case <-time.After(rateLimitedError.RetryAfter): + err = nil + } + } + } + + return results, p.Failure(err) +} + +// SetChannelReadMark sets the read mark of a given channel to a specific point +// Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a +// timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls +// (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A +// timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. +// see https://api.slack.com/methods/channels.mark +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) SetChannelReadMark(channelID, ts string) error { + return api.SetChannelReadMarkContext(context.Background(), channelID, ts) +} + +// SetChannelReadMarkContext sets the read mark of a given channel to a specific point with a custom context +// For more details see SetChannelReadMark documentation +// see https://api.slack.com/methods/channels.mark +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts string) (err error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "ts": {ts}, + } + + _, err = api.channelRequest(ctx, "channels.mark", values) + return err +} + +// RenameChannel renames a given channel +// see https://api.slack.com/methods/channels.rename +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) RenameChannel(channelID, name string) (*Channel, error) { + return api.RenameChannelContext(context.Background(), channelID, name) +} + +// RenameChannelContext renames a given channel with a custom context +// see https://api.slack.com/methods/channels.rename +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) RenameChannelContext(ctx context.Context, channelID, name string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "name": {name}, + } + + // XXX: the created entry in this call returns a string instead of a number + // so I may have to do some workaround to solve it. + response, err := api.channelRequest(ctx, "channels.rename", values) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// SetChannelPurpose sets the channel purpose and returns the purpose that was successfully set +// see https://api.slack.com/methods/channels.setPurpose +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) SetChannelPurpose(channelID, purpose string) (string, error) { + return api.SetChannelPurposeContext(context.Background(), channelID, purpose) +} + +// SetChannelPurposeContext sets the channel purpose and returns the purpose that was successfully set with a custom context +// see https://api.slack.com/methods/channels.setPurpose +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purpose string) (string, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "purpose": {purpose}, + } + + response, err := api.channelRequest(ctx, "channels.setPurpose", values) + if err != nil { + return "", err + } + return response.Purpose, nil +} + +// SetChannelTopic sets the channel topic and returns the topic that was successfully set +// see https://api.slack.com/methods/channels.setTopic +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) SetChannelTopic(channelID, topic string) (string, error) { + return api.SetChannelTopicContext(context.Background(), channelID, topic) +} + +// SetChannelTopicContext sets the channel topic and returns the topic that was successfully set with a custom context +// see https://api.slack.com/methods/channels.setTopic +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic string) (string, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "topic": {topic}, + } + + response, err := api.channelRequest(ctx, "channels.setTopic", values) + if err != nil { + return "", err + } + return response.Topic, nil +} + +// GetChannelReplies gets an entire thread (a message plus all the messages in reply to it). +// see https://api.slack.com/methods/channels.replies +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetChannelReplies(channelID, thread_ts string) ([]Message, error) { + return api.GetChannelRepliesContext(context.Background(), channelID, thread_ts) +} + +// GetChannelRepliesContext gets an entire thread (a message plus all the messages in reply to it) with a custom context +// see https://api.slack.com/methods/channels.replies +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "thread_ts": {thread_ts}, + } + response, err := api.channelRequest(ctx, "channels.replies", values) + if err != nil { + return nil, err + } + return response.History.Messages, nil +} diff --git a/backend/vendor/github.com/slack-go/slack/chat.go b/backend/vendor/github.com/slack-go/slack/chat.go new file mode 100644 index 00000000..f5b80346 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/chat.go @@ -0,0 +1,840 @@ +package slack + +import ( + "bytes" + "context" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strconv" + + "github.com/slack-go/slack/slackutilsx" +) + +const ( + DEFAULT_MESSAGE_USERNAME = "" + DEFAULT_MESSAGE_REPLY_BROADCAST = false + DEFAULT_MESSAGE_ASUSER = false + DEFAULT_MESSAGE_PARSE = "" + DEFAULT_MESSAGE_THREAD_TIMESTAMP = "" + DEFAULT_MESSAGE_LINK_NAMES = 0 + DEFAULT_MESSAGE_UNFURL_LINKS = false + DEFAULT_MESSAGE_UNFURL_MEDIA = true + DEFAULT_MESSAGE_ICON_URL = "" + DEFAULT_MESSAGE_ICON_EMOJI = "" + DEFAULT_MESSAGE_MARKDOWN = true + DEFAULT_MESSAGE_ESCAPE_TEXT = true +) + +type chatResponseFull struct { + Channel string `json:"channel"` + Timestamp string `json:"ts"` //Regular message timestamp + MessageTimeStamp string `json:"message_ts"` //Ephemeral message timestamp + ScheduledMessageID string `json:"scheduled_message_id,omitempty"` //Scheduled message id + Text string `json:"text"` + SlackResponse +} + +// getMessageTimestamp will inspect the `chatResponseFull` to ruturn a timestamp value +// in `chat.postMessage` its under `ts` +// in `chat.postEphemeral` its under `message_ts` +func (c chatResponseFull) getMessageTimestamp() string { + if len(c.Timestamp) > 0 { + return c.Timestamp + } + return c.MessageTimeStamp +} + +// PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request +type PostMessageParameters struct { + Username string `json:"username"` + AsUser bool `json:"as_user"` + Parse string `json:"parse"` + ThreadTimestamp string `json:"thread_ts"` + ReplyBroadcast bool `json:"reply_broadcast"` + LinkNames int `json:"link_names"` + UnfurlLinks bool `json:"unfurl_links"` + UnfurlMedia bool `json:"unfurl_media"` + IconURL string `json:"icon_url"` + IconEmoji string `json:"icon_emoji"` + Markdown bool `json:"mrkdwn,omitempty"` + EscapeText bool `json:"escape_text"` + + // chat.postEphemeral support + Channel string `json:"channel"` + User string `json:"user"` +} + +// NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set +func NewPostMessageParameters() PostMessageParameters { + return PostMessageParameters{ + Username: DEFAULT_MESSAGE_USERNAME, + User: DEFAULT_MESSAGE_USERNAME, + AsUser: DEFAULT_MESSAGE_ASUSER, + Parse: DEFAULT_MESSAGE_PARSE, + ThreadTimestamp: DEFAULT_MESSAGE_THREAD_TIMESTAMP, + LinkNames: DEFAULT_MESSAGE_LINK_NAMES, + UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS, + UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA, + IconURL: DEFAULT_MESSAGE_ICON_URL, + IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI, + Markdown: DEFAULT_MESSAGE_MARKDOWN, + EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT, + } +} + +// DeleteMessage deletes a message in a channel +func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + context.Background(), + channel, + MsgOptionDelete(messageTimestamp), + ) + return respChannel, respTimestamp, err +} + +// DeleteMessageContext deletes a message in a channel with a custom context +func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTimestamp string) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + ctx, + channel, + MsgOptionDelete(messageTimestamp), + ) + return respChannel, respTimestamp, err +} + +// ScheduleMessage sends a message to a channel. +// Message is escaped by default according to https://api.slack.com/docs/formatting +// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. +func (api *Client) ScheduleMessage(channelID, postAt string, options ...MsgOption) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + context.Background(), + channelID, + MsgOptionSchedule(postAt), + MsgOptionCompose(options...), + ) + return respChannel, respTimestamp, err +} + +// PostMessage sends a message to a channel. +// Message is escaped by default according to https://api.slack.com/docs/formatting +// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. +func (api *Client) PostMessage(channelID string, options ...MsgOption) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + context.Background(), + channelID, + MsgOptionPost(), + MsgOptionCompose(options...), + ) + return respChannel, respTimestamp, err +} + +// PostMessageContext sends a message to a channel with a custom context +// For more details, see PostMessage documentation. +func (api *Client) PostMessageContext(ctx context.Context, channelID string, options ...MsgOption) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + ctx, + channelID, + MsgOptionPost(), + MsgOptionCompose(options...), + ) + return respChannel, respTimestamp, err +} + +// PostEphemeral sends an ephemeral message to a user in a channel. +// Message is escaped by default according to https://api.slack.com/docs/formatting +// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. +func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) (string, error) { + return api.PostEphemeralContext( + context.Background(), + channelID, + userID, + options..., + ) +} + +// PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context +// For more details, see PostEphemeral documentation +func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID string, options ...MsgOption) (timestamp string, err error) { + _, timestamp, _, err = api.SendMessageContext( + ctx, + channelID, + MsgOptionPostEphemeral(userID), + MsgOptionCompose(options...), + ) + return timestamp, err +} + +// UpdateMessage updates a message in a channel +func (api *Client) UpdateMessage(channelID, timestamp string, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext( + context.Background(), + channelID, + MsgOptionUpdate(timestamp), + MsgOptionCompose(options...), + ) +} + +// UpdateMessageContext updates a message in a channel +func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext( + ctx, + channelID, + MsgOptionUpdate(timestamp), + MsgOptionCompose(options...), + ) +} + +// UnfurlMessage unfurls a message in a channel +func (api *Client) UnfurlMessage(channelID, timestamp string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(context.Background(), channelID, MsgOptionUnfurl(timestamp, unfurls), MsgOptionCompose(options...)) +} + +// UnfurlMessageWithAuthURL sends an unfurl request containing an +// authentication URL. +// For more details see: +// https://api.slack.com/reference/messaging/link-unfurling#authenticated_unfurls +func (api *Client) UnfurlMessageWithAuthURL(channelID, timestamp string, userAuthURL string, options ...MsgOption) (string, string, string, error) { + return api.UnfurlMessageWithAuthURLContext(context.Background(), channelID, timestamp, userAuthURL, options...) +} + +// UnfurlMessageWithAuthURLContext sends an unfurl request containing an +// authentication URL. +// For more details see: +// https://api.slack.com/reference/messaging/link-unfurling#authenticated_unfurls +func (api *Client) UnfurlMessageWithAuthURLContext(ctx context.Context, channelID, timestamp string, userAuthURL string, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(ctx, channelID, MsgOptionUnfurlAuthURL(timestamp, userAuthURL), MsgOptionCompose(options...)) +} + +// SendMessage more flexible method for configuring messages. +func (api *Client) SendMessage(channel string, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(context.Background(), channel, options...) +} + +// SendMessageContext more flexible method for configuring messages with a custom context. +func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestamp string, _text string, err error) { + var ( + req *http.Request + parser func(*chatResponseFull) responseParser + response chatResponseFull + ) + + if req, parser, err = buildSender(api.endpoint, options...).BuildRequest(api.token, channelID); err != nil { + return "", "", "", err + } + + if api.Debug() { + reqBody, err := ioutil.ReadAll(req.Body) + if err != nil { + return "", "", "", err + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) + api.Debugf("Sending request: %s", string(reqBody)) + } + + if err = doPost(ctx, api.httpclient, req, parser(&response), api); err != nil { + return "", "", "", err + } + + return response.Channel, response.getMessageTimestamp(), response.Text, response.Err() +} + +// UnsafeApplyMsgOptions utility function for debugging/testing chat requests. +// NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this function +// will be supported by the library. +func UnsafeApplyMsgOptions(token, channel, apiurl string, options ...MsgOption) (string, url.Values, error) { + config, err := applyMsgOptions(token, channel, apiurl, options...) + return config.endpoint, config.values, err +} + +func applyMsgOptions(token, channel, apiurl string, options ...MsgOption) (sendConfig, error) { + config := sendConfig{ + apiurl: apiurl, + endpoint: apiurl + string(chatPostMessage), + values: url.Values{ + "token": {token}, + "channel": {channel}, + }, + } + + for _, opt := range options { + if err := opt(&config); err != nil { + return config, err + } + } + + return config, nil +} + +func buildSender(apiurl string, options ...MsgOption) sendConfig { + return sendConfig{ + apiurl: apiurl, + options: options, + } +} + +type sendMode string + +const ( + chatUpdate sendMode = "chat.update" + chatPostMessage sendMode = "chat.postMessage" + chatScheduleMessage sendMode = "chat.scheduleMessage" + chatDelete sendMode = "chat.delete" + chatPostEphemeral sendMode = "chat.postEphemeral" + chatResponse sendMode = "chat.responseURL" + chatMeMessage sendMode = "chat.meMessage" + chatUnfurl sendMode = "chat.unfurl" +) + +type sendConfig struct { + apiurl string + options []MsgOption + mode sendMode + endpoint string + values url.Values + attachments []Attachment + blocks Blocks + responseType string + replaceOriginal bool + deleteOriginal bool +} + +func (t sendConfig) BuildRequest(token, channelID string) (req *http.Request, _ func(*chatResponseFull) responseParser, err error) { + if t, err = applyMsgOptions(token, channelID, t.apiurl, t.options...); err != nil { + return nil, nil, err + } + + switch t.mode { + case chatResponse: + return responseURLSender{ + endpoint: t.endpoint, + values: t.values, + attachments: t.attachments, + blocks: t.blocks, + responseType: t.responseType, + replaceOriginal: t.replaceOriginal, + deleteOriginal: t.deleteOriginal, + }.BuildRequest() + default: + return formSender{endpoint: t.endpoint, values: t.values}.BuildRequest() + } +} + +type formSender struct { + endpoint string + values url.Values +} + +func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { + req, err := formReq(t.endpoint, t.values) + return req, func(resp *chatResponseFull) responseParser { + return newJSONParser(resp) + }, err +} + +type responseURLSender struct { + endpoint string + values url.Values + attachments []Attachment + blocks Blocks + responseType string + replaceOriginal bool + deleteOriginal bool +} + +func (t responseURLSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { + req, err := jsonReq(t.endpoint, Msg{ + Text: t.values.Get("text"), + Timestamp: t.values.Get("ts"), + Attachments: t.attachments, + Blocks: t.blocks, + ResponseType: t.responseType, + ReplaceOriginal: t.replaceOriginal, + DeleteOriginal: t.deleteOriginal, + }) + return req, func(resp *chatResponseFull) responseParser { + return newContentTypeParser(resp) + }, err +} + +// MsgOption option provided when sending a message. +type MsgOption func(*sendConfig) error + +// MsgOptionSchedule schedules a messages. +func MsgOptionSchedule(postAt string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatScheduleMessage) + config.values.Add("post_at", postAt) + return nil + } +} + +// MsgOptionPost posts a messages, this is the default. +func MsgOptionPost() MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatPostMessage) + config.values.Del("ts") + return nil + } +} + +// MsgOptionPostEphemeral - posts an ephemeral message to the provided user. +func MsgOptionPostEphemeral(userID string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatPostEphemeral) + MsgOptionUser(userID)(config) + config.values.Del("ts") + + return nil + } +} + +// MsgOptionMeMessage posts a "me message" type from the calling user +func MsgOptionMeMessage() MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatMeMessage) + return nil + } +} + +// MsgOptionUpdate updates a message based on the timestamp. +func MsgOptionUpdate(timestamp string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUpdate) + config.values.Add("ts", timestamp) + return nil + } +} + +// MsgOptionDelete deletes a message based on the timestamp. +func MsgOptionDelete(timestamp string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatDelete) + config.values.Add("ts", timestamp) + return nil + } +} + +// MsgOptionUnfurl unfurls a message based on the timestamp. +func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Add("ts", timestamp) + unfurlsStr, err := json.Marshal(unfurls) + if err == nil { + config.values.Add("unfurls", string(unfurlsStr)) + } + return err + } +} + +// MsgOptionUnfurlAuthURL unfurls a message using an auth url based on the timestamp. +func MsgOptionUnfurlAuthURL(timestamp string, userAuthURL string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Add("ts", timestamp) + config.values.Add("user_auth_url", userAuthURL) + return nil + } +} + +// MsgOptionUnfurlAuthRequired requests that the user installs the +// Slack app for unfurling. +func MsgOptionUnfurlAuthRequired(timestamp string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Add("ts", timestamp) + config.values.Add("user_auth_required", "true") + return nil + } +} + +// MsgOptionUnfurlAuthMessage attaches a message inviting the user to +// authenticate. +func MsgOptionUnfurlAuthMessage(timestamp string, msg string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Add("ts", timestamp) + config.values.Add("user_auth_message", msg) + return nil + } +} + +// MsgOptionResponseURL supplies a url to use as the endpoint. +func MsgOptionResponseURL(url string, responseType string) MsgOption { + return func(config *sendConfig) error { + config.mode = chatResponse + config.endpoint = url + config.responseType = responseType + config.values.Del("ts") + return nil + } +} + +// MsgOptionReplaceOriginal replaces original message with response url +func MsgOptionReplaceOriginal(responseURL string) MsgOption { + return func(config *sendConfig) error { + config.mode = chatResponse + config.endpoint = responseURL + config.replaceOriginal = true + return nil + } +} + +// MsgOptionDeleteOriginal deletes original message with response url +func MsgOptionDeleteOriginal(responseURL string) MsgOption { + return func(config *sendConfig) error { + config.mode = chatResponse + config.endpoint = responseURL + config.deleteOriginal = true + return nil + } +} + +// MsgOptionAsUser whether or not to send the message as the user. +func MsgOptionAsUser(b bool) MsgOption { + return func(config *sendConfig) error { + if b != DEFAULT_MESSAGE_ASUSER { + config.values.Set("as_user", "true") + } + return nil + } +} + +// MsgOptionUser set the user for the message. +func MsgOptionUser(userID string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("user", userID) + return nil + } +} + +// MsgOptionUsername set the username for the message. +func MsgOptionUsername(username string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("username", username) + return nil + } +} + +// MsgOptionText provide the text for the message, optionally escape the provided +// text. +func MsgOptionText(text string, escape bool) MsgOption { + return func(config *sendConfig) error { + if escape { + text = slackutilsx.EscapeMessage(text) + } + config.values.Add("text", text) + return nil + } +} + +// MsgOptionAttachments provide attachments for the message. +func MsgOptionAttachments(attachments ...Attachment) MsgOption { + return func(config *sendConfig) error { + if attachments == nil { + return nil + } + + config.attachments = attachments + + // FIXME: We are setting the attachments on the message twice: above for + // the json version, and below for the html version. The marshalled bytes + // we put into config.values below don't work directly in the Msg version. + + attachmentBytes, err := json.Marshal(attachments) + if err == nil { + config.values.Set("attachments", string(attachmentBytes)) + } + + return err + } +} + +// MsgOptionBlocks sets blocks for the message +func MsgOptionBlocks(blocks ...Block) MsgOption { + return func(config *sendConfig) error { + if blocks == nil { + return nil + } + + config.blocks.BlockSet = append(config.blocks.BlockSet, blocks...) + + blocks, err := json.Marshal(blocks) + if err == nil { + config.values.Set("blocks", string(blocks)) + } + return err + } +} + +// MsgOptionEnableLinkUnfurl enables link unfurling +func MsgOptionEnableLinkUnfurl() MsgOption { + return func(config *sendConfig) error { + config.values.Set("unfurl_links", "true") + return nil + } +} + +// MsgOptionDisableLinkUnfurl disables link unfurling +func MsgOptionDisableLinkUnfurl() MsgOption { + return func(config *sendConfig) error { + config.values.Set("unfurl_links", "false") + return nil + } +} + +// MsgOptionDisableMediaUnfurl disables media unfurling. +func MsgOptionDisableMediaUnfurl() MsgOption { + return func(config *sendConfig) error { + config.values.Set("unfurl_media", "false") + return nil + } +} + +// MsgOptionDisableMarkdown disables markdown. +func MsgOptionDisableMarkdown() MsgOption { + return func(config *sendConfig) error { + config.values.Set("mrkdwn", "false") + return nil + } +} + +// MsgOptionTS sets the thread TS of the message to enable creating or replying to a thread +func MsgOptionTS(ts string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("thread_ts", ts) + return nil + } +} + +// MsgOptionBroadcast sets reply_broadcast to true +func MsgOptionBroadcast() MsgOption { + return func(config *sendConfig) error { + config.values.Set("reply_broadcast", "true") + return nil + } +} + +// MsgOptionCompose combines multiple options into a single option. +func MsgOptionCompose(options ...MsgOption) MsgOption { + return func(config *sendConfig) error { + for _, opt := range options { + if err := opt(config); err != nil { + return err + } + } + return nil + } +} + +// MsgOptionParse set parse option. +func MsgOptionParse(b bool) MsgOption { + return func(config *sendConfig) error { + var v string + if b { + v = "full" + } else { + v = "none" + } + config.values.Set("parse", v) + return nil + } +} + +// MsgOptionIconURL sets an icon URL +func MsgOptionIconURL(iconURL string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("icon_url", iconURL) + return nil + } +} + +// MsgOptionIconEmoji sets an icon emoji +func MsgOptionIconEmoji(iconEmoji string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("icon_emoji", iconEmoji) + return nil + } +} + +// UnsafeMsgOptionEndpoint deliver the message to the specified endpoint. +// NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this Option +// will be supported by the library, it is subject to change without notice that +// may result in compilation errors or runtime behaviour changes. +func UnsafeMsgOptionEndpoint(endpoint string, update func(url.Values)) MsgOption { + return func(config *sendConfig) error { + config.endpoint = endpoint + update(config.values) + return nil + } +} + +// MsgOptionPostMessageParameters maintain backwards compatibility. +func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { + return func(config *sendConfig) error { + if params.Username != DEFAULT_MESSAGE_USERNAME { + config.values.Set("username", params.Username) + } + + // chat.postEphemeral support + if params.User != DEFAULT_MESSAGE_USERNAME { + config.values.Set("user", params.User) + } + + // never generates an error. + MsgOptionAsUser(params.AsUser)(config) + + if params.Parse != DEFAULT_MESSAGE_PARSE { + config.values.Set("parse", params.Parse) + } + if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { + config.values.Set("link_names", "1") + } + + if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS { + config.values.Set("unfurl_links", "true") + } + + // I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request. + // Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side. + if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS { + config.values.Set("unfurl_links", "false") + } + if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA { + config.values.Set("unfurl_media", "false") + } + if params.IconURL != DEFAULT_MESSAGE_ICON_URL { + config.values.Set("icon_url", params.IconURL) + } + if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI { + config.values.Set("icon_emoji", params.IconEmoji) + } + if params.Markdown != DEFAULT_MESSAGE_MARKDOWN { + config.values.Set("mrkdwn", "false") + } + + if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP { + config.values.Set("thread_ts", params.ThreadTimestamp) + } + if params.ReplyBroadcast != DEFAULT_MESSAGE_REPLY_BROADCAST { + config.values.Set("reply_broadcast", "true") + } + + return nil + } +} + +// PermalinkParameters are the parameters required to get a permalink to a +// message. Slack documentation can be found here: +// https://api.slack.com/methods/chat.getPermalink +type PermalinkParameters struct { + Channel string + Ts string +} + +// GetPermalink returns the permalink for a message. It takes +// PermalinkParameters and returns a string containing the permalink. It +// returns an error if unable to retrieve the permalink. +func (api *Client) GetPermalink(params *PermalinkParameters) (string, error) { + return api.GetPermalinkContext(context.Background(), params) +} + +// GetPermalinkContext returns the permalink for a message using a custom context. +func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkParameters) (string, error) { + values := url.Values{ + "token": {api.token}, + "channel": {params.Channel}, + "message_ts": {params.Ts}, + } + + response := struct { + Channel string `json:"channel"` + Permalink string `json:"permalink"` + SlackResponse + }{} + err := api.getMethod(ctx, "chat.getPermalink", values, &response) + if err != nil { + return "", err + } + return response.Permalink, response.Err() +} + +type GetScheduledMessagesParameters struct { + Channel string + Cursor string + Latest string + Limit int + Oldest string +} + +// GetScheduledMessages returns the list of scheduled messages based on params +func (api *Client) GetScheduledMessages(params *GetScheduledMessagesParameters) (channels []ScheduledMessage, nextCursor string, err error) { + return api.GetScheduledMessagesContext(context.Background(), params) +} + +// GetScheduledMessagesContext returns the list of scheduled messages in a Slack team with a custom context +func (api *Client) GetScheduledMessagesContext(ctx context.Context, params *GetScheduledMessagesParameters) (channels []ScheduledMessage, nextCursor string, err error) { + values := url.Values{ + "token": {api.token}, + } + if params.Channel != "" { + values.Add("channel", params.Channel) + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Latest != "" { + values.Add("latest", params.Latest) + } + if params.Oldest != "" { + values.Add("oldest", params.Oldest) + } + response := struct { + Messages []ScheduledMessage `json:"scheduled_messages"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + + err = api.postMethod(ctx, "chat.scheduledMessages.list", values, &response) + if err != nil { + return nil, "", err + } + + return response.Messages, response.ResponseMetaData.NextCursor, response.Err() +} + +type DeleteScheduledMessageParameters struct { + Channel string + ScheduledMessageID string + AsUser bool +} + +// DeleteScheduledMessage returns the list of scheduled messages based on params +func (api *Client) DeleteScheduledMessage(params *DeleteScheduledMessageParameters) (bool, error) { + return api.DeleteScheduledMessageContext(context.Background(), params) +} + +// DeleteScheduledMessageContext returns the list of scheduled messages in a Slack team with a custom context +func (api *Client) DeleteScheduledMessageContext(ctx context.Context, params *DeleteScheduledMessageParameters) (bool, error) { + values := url.Values{ + "token": {api.token}, + "channel": {params.Channel}, + "scheduled_message_id": {params.ScheduledMessageID}, + "as_user": {strconv.FormatBool(params.AsUser)}, + } + response := struct { + SlackResponse + }{} + + err := api.postMethod(ctx, "chat.deleteScheduledMessage", values, &response) + if err != nil { + return false, err + } + + return response.Ok, response.Err() +} diff --git a/backend/vendor/github.com/slack-go/slack/comment.go b/backend/vendor/github.com/slack-go/slack/comment.go new file mode 100644 index 00000000..7d1c0d4e --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/comment.go @@ -0,0 +1,10 @@ +package slack + +// Comment contains all the information relative to a comment +type Comment struct { + ID string `json:"id,omitempty"` + Created JSONTime `json:"created,omitempty"` + Timestamp JSONTime `json:"timestamp,omitempty"` + User string `json:"user,omitempty"` + Comment string `json:"comment,omitempty"` +} diff --git a/backend/vendor/github.com/slack-go/slack/conversation.go b/backend/vendor/github.com/slack-go/slack/conversation.go new file mode 100644 index 00000000..6fc4a083 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/conversation.go @@ -0,0 +1,645 @@ +package slack + +import ( + "context" + "net/url" + "strconv" + "strings" +) + +// Conversation is the foundation for IM and BaseGroupConversation +type Conversation struct { + ID string `json:"id"` + Created JSONTime `json:"created"` + IsOpen bool `json:"is_open"` + LastRead string `json:"last_read,omitempty"` + Latest *Message `json:"latest,omitempty"` + UnreadCount int `json:"unread_count,omitempty"` + UnreadCountDisplay int `json:"unread_count_display,omitempty"` + IsGroup bool `json:"is_group"` + IsShared bool `json:"is_shared"` + IsIM bool `json:"is_im"` + IsExtShared bool `json:"is_ext_shared"` + IsOrgShared bool `json:"is_org_shared"` + IsPendingExtShared bool `json:"is_pending_ext_shared"` + IsPrivate bool `json:"is_private"` + IsMpIM bool `json:"is_mpim"` + Unlinked int `json:"unlinked"` + NameNormalized string `json:"name_normalized"` + NumMembers int `json:"num_members"` + Priority float64 `json:"priority"` + User string `json:"user"` + + // TODO support pending_shared + // TODO support previous_names +} + +// GroupConversation is the foundation for Group and Channel +type GroupConversation struct { + Conversation + Name string `json:"name"` + Creator string `json:"creator"` + IsArchived bool `json:"is_archived"` + Members []string `json:"members"` + Topic Topic `json:"topic"` + Purpose Purpose `json:"purpose"` +} + +// Topic contains information about the topic +type Topic struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet JSONTime `json:"last_set"` +} + +// Purpose contains information about the purpose +type Purpose struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet JSONTime `json:"last_set"` +} + +type GetUsersInConversationParameters struct { + ChannelID string + Cursor string + Limit int +} + +type GetConversationsForUserParameters struct { + UserID string + Cursor string + Types []string + Limit int + ExcludeArchived bool +} + +type responseMetaData struct { + NextCursor string `json:"next_cursor"` +} + +// GetUsersInConversation returns the list of users in a conversation +func (api *Client) GetUsersInConversation(params *GetUsersInConversationParameters) ([]string, string, error) { + return api.GetUsersInConversationContext(context.Background(), params) +} + +// GetUsersInConversationContext returns the list of users in a conversation with a custom context +func (api *Client) GetUsersInConversationContext(ctx context.Context, params *GetUsersInConversationParameters) ([]string, string, error) { + values := url.Values{ + "token": {api.token}, + "channel": {params.ChannelID}, + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + response := struct { + Members []string `json:"members"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + + err := api.postMethod(ctx, "conversations.members", values, &response) + if err != nil { + return nil, "", err + } + + if err := response.Err(); err != nil { + return nil, "", err + } + + return response.Members, response.ResponseMetaData.NextCursor, nil +} + +// GetConversationsForUser returns the list conversations for a given user +func (api *Client) GetConversationsForUser(params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) { + return api.GetConversationsForUserContext(context.Background(), params) +} + +// GetConversationsForUserContext returns the list conversations for a given user with a custom context +func (api *Client) GetConversationsForUserContext(ctx context.Context, params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) { + values := url.Values{ + "token": {api.token}, + } + if params.UserID != "" { + values.Add("user", params.UserID) + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Types != nil { + values.Add("types", strings.Join(params.Types, ",")) + } + if params.ExcludeArchived { + values.Add("exclude_archived", "true") + } + response := struct { + Channels []Channel `json:"channels"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + err = api.postMethod(ctx, "users.conversations", values, &response) + if err != nil { + return nil, "", err + } + + return response.Channels, response.ResponseMetaData.NextCursor, response.Err() +} + +// ArchiveConversation archives a conversation +func (api *Client) ArchiveConversation(channelID string) error { + return api.ArchiveConversationContext(context.Background(), channelID) +} + +// ArchiveConversationContext archives a conversation with a custom context +func (api *Client) ArchiveConversationContext(ctx context.Context, channelID string) error { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + + response := SlackResponse{} + err := api.postMethod(ctx, "conversations.archive", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// UnArchiveConversation reverses conversation archival +func (api *Client) UnArchiveConversation(channelID string) error { + return api.UnArchiveConversationContext(context.Background(), channelID) +} + +// UnArchiveConversationContext reverses conversation archival with a custom context +func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID string) error { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + response := SlackResponse{} + err := api.postMethod(ctx, "conversations.unarchive", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// SetTopicOfConversation sets the topic for a conversation +func (api *Client) SetTopicOfConversation(channelID, topic string) (*Channel, error) { + return api.SetTopicOfConversationContext(context.Background(), channelID, topic) +} + +// SetTopicOfConversationContext sets the topic for a conversation with a custom context +func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID, topic string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "topic": {topic}, + } + response := struct { + SlackResponse + Channel *Channel `json:"channel"` + }{} + err := api.postMethod(ctx, "conversations.setTopic", values, &response) + if err != nil { + return nil, err + } + + return response.Channel, response.Err() +} + +// SetPurposeOfConversation sets the purpose for a conversation +func (api *Client) SetPurposeOfConversation(channelID, purpose string) (*Channel, error) { + return api.SetPurposeOfConversationContext(context.Background(), channelID, purpose) +} + +// SetPurposeOfConversationContext sets the purpose for a conversation with a custom context +func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelID, purpose string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "purpose": {purpose}, + } + response := struct { + SlackResponse + Channel *Channel `json:"channel"` + }{} + + err := api.postMethod(ctx, "conversations.setPurpose", values, &response) + if err != nil { + return nil, err + } + + return response.Channel, response.Err() +} + +// RenameConversation renames a conversation +func (api *Client) RenameConversation(channelID, channelName string) (*Channel, error) { + return api.RenameConversationContext(context.Background(), channelID, channelName) +} + +// RenameConversationContext renames a conversation with a custom context +func (api *Client) RenameConversationContext(ctx context.Context, channelID, channelName string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "name": {channelName}, + } + response := struct { + SlackResponse + Channel *Channel `json:"channel"` + }{} + + err := api.postMethod(ctx, "conversations.rename", values, &response) + if err != nil { + return nil, err + } + + return response.Channel, response.Err() +} + +// InviteUsersToConversation invites users to a channel +func (api *Client) InviteUsersToConversation(channelID string, users ...string) (*Channel, error) { + return api.InviteUsersToConversationContext(context.Background(), channelID, users...) +} + +// InviteUsersToConversationContext invites users to a channel with a custom context +func (api *Client) InviteUsersToConversationContext(ctx context.Context, channelID string, users ...string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "users": {strings.Join(users, ",")}, + } + response := struct { + SlackResponse + Channel *Channel `json:"channel"` + }{} + + err := api.postMethod(ctx, "conversations.invite", values, &response) + if err != nil { + return nil, err + } + + return response.Channel, response.Err() +} + +// KickUserFromConversation removes a user from a conversation +func (api *Client) KickUserFromConversation(channelID string, user string) error { + return api.KickUserFromConversationContext(context.Background(), channelID, user) +} + +// KickUserFromConversationContext removes a user from a conversation with a custom context +func (api *Client) KickUserFromConversationContext(ctx context.Context, channelID string, user string) error { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "user": {user}, + } + + response := SlackResponse{} + err := api.postMethod(ctx, "conversations.kick", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// CloseConversation closes a direct message or multi-person direct message +func (api *Client) CloseConversation(channelID string) (noOp bool, alreadyClosed bool, err error) { + return api.CloseConversationContext(context.Background(), channelID) +} + +// CloseConversationContext closes a direct message or multi-person direct message with a custom context +func (api *Client) CloseConversationContext(ctx context.Context, channelID string) (noOp bool, alreadyClosed bool, err error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + response := struct { + SlackResponse + NoOp bool `json:"no_op"` + AlreadyClosed bool `json:"already_closed"` + }{} + + err = api.postMethod(ctx, "conversations.close", values, &response) + if err != nil { + return false, false, err + } + + return response.NoOp, response.AlreadyClosed, response.Err() +} + +// CreateConversation initiates a public or private channel-based conversation +func (api *Client) CreateConversation(channelName string, isPrivate bool) (*Channel, error) { + return api.CreateConversationContext(context.Background(), channelName, isPrivate) +} + +// CreateConversationContext initiates a public or private channel-based conversation with a custom context +func (api *Client) CreateConversationContext(ctx context.Context, channelName string, isPrivate bool) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "name": {channelName}, + "is_private": {strconv.FormatBool(isPrivate)}, + } + response, err := api.channelRequest(ctx, "conversations.create", values) + if err != nil { + return nil, err + } + + return &response.Channel, nil +} + +// GetConversationInfo retrieves information about a conversation +func (api *Client) GetConversationInfo(channelID string, includeLocale bool) (*Channel, error) { + return api.GetConversationInfoContext(context.Background(), channelID, includeLocale) +} + +// GetConversationInfoContext retrieves information about a conversation with a custom context +func (api *Client) GetConversationInfoContext(ctx context.Context, channelID string, includeLocale bool) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "include_locale": {strconv.FormatBool(includeLocale)}, + } + response, err := api.channelRequest(ctx, "conversations.info", values) + if err != nil { + return nil, err + } + + return &response.Channel, response.Err() +} + +// LeaveConversation leaves a conversation +func (api *Client) LeaveConversation(channelID string) (bool, error) { + return api.LeaveConversationContext(context.Background(), channelID) +} + +// LeaveConversationContext leaves a conversation with a custom context +func (api *Client) LeaveConversationContext(ctx context.Context, channelID string) (bool, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + } + + response, err := api.channelRequest(ctx, "conversations.leave", values) + if err != nil { + return false, err + } + + return response.NotInChannel, err +} + +type GetConversationRepliesParameters struct { + ChannelID string + Timestamp string + Cursor string + Inclusive bool + Latest string + Limit int + Oldest string +} + +// GetConversationReplies retrieves a thread of messages posted to a conversation +func (api *Client) GetConversationReplies(params *GetConversationRepliesParameters) (msgs []Message, hasMore bool, nextCursor string, err error) { + return api.GetConversationRepliesContext(context.Background(), params) +} + +// GetConversationRepliesContext retrieves a thread of messages posted to a conversation with a custom context +func (api *Client) GetConversationRepliesContext(ctx context.Context, params *GetConversationRepliesParameters) (msgs []Message, hasMore bool, nextCursor string, err error) { + values := url.Values{ + "token": {api.token}, + "channel": {params.ChannelID}, + "ts": {params.Timestamp}, + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Latest != "" { + values.Add("latest", params.Latest) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Oldest != "" { + values.Add("oldest", params.Oldest) + } + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + response := struct { + SlackResponse + HasMore bool `json:"has_more"` + ResponseMetaData struct { + NextCursor string `json:"next_cursor"` + } `json:"response_metadata"` + Messages []Message `json:"messages"` + }{} + + err = api.postMethod(ctx, "conversations.replies", values, &response) + if err != nil { + return nil, false, "", err + } + + return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, response.Err() +} + +type GetConversationsParameters struct { + Cursor string + ExcludeArchived string + Limit int + Types []string +} + +// GetConversations returns the list of channels in a Slack team +func (api *Client) GetConversations(params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) { + return api.GetConversationsContext(context.Background(), params) +} + +// GetConversationsContext returns the list of channels in a Slack team with a custom context +func (api *Client) GetConversationsContext(ctx context.Context, params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) { + values := url.Values{ + "token": {api.token}, + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Types != nil { + values.Add("types", strings.Join(params.Types, ",")) + } + if params.ExcludeArchived == "true" { + values.Add("exclude_archived", "true") + } + + response := struct { + Channels []Channel `json:"channels"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + + err = api.postMethod(ctx, "conversations.list", values, &response) + if err != nil { + return nil, "", err + } + + return response.Channels, response.ResponseMetaData.NextCursor, response.Err() +} + +type OpenConversationParameters struct { + ChannelID string + ReturnIM bool + Users []string +} + +// OpenConversation opens or resumes a direct message or multi-person direct message +func (api *Client) OpenConversation(params *OpenConversationParameters) (*Channel, bool, bool, error) { + return api.OpenConversationContext(context.Background(), params) +} + +// OpenConversationContext opens or resumes a direct message or multi-person direct message with a custom context +func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConversationParameters) (*Channel, bool, bool, error) { + values := url.Values{ + "token": {api.token}, + "return_im": {strconv.FormatBool(params.ReturnIM)}, + } + if params.ChannelID != "" { + values.Add("channel", params.ChannelID) + } + if params.Users != nil { + values.Add("users", strings.Join(params.Users, ",")) + } + response := struct { + Channel *Channel `json:"channel"` + NoOp bool `json:"no_op"` + AlreadyOpen bool `json:"already_open"` + SlackResponse + }{} + + err := api.postMethod(ctx, "conversations.open", values, &response) + if err != nil { + return nil, false, false, err + } + + return response.Channel, response.NoOp, response.AlreadyOpen, response.Err() +} + +// JoinConversation joins an existing conversation +func (api *Client) JoinConversation(channelID string) (*Channel, string, []string, error) { + return api.JoinConversationContext(context.Background(), channelID) +} + +// JoinConversationContext joins an existing conversation with a custom context +func (api *Client) JoinConversationContext(ctx context.Context, channelID string) (*Channel, string, []string, error) { + values := url.Values{"token": {api.token}, "channel": {channelID}} + response := struct { + Channel *Channel `json:"channel"` + Warning string `json:"warning"` + ResponseMetaData *struct { + Warnings []string `json:"warnings"` + } `json:"response_metadata"` + SlackResponse + }{} + + err := api.postMethod(ctx, "conversations.join", values, &response) + if err != nil { + return nil, "", nil, err + } + if response.Err() != nil { + return nil, "", nil, response.Err() + } + var warnings []string + if response.ResponseMetaData != nil { + warnings = response.ResponseMetaData.Warnings + } + return response.Channel, response.Warning, warnings, nil +} + +type GetConversationHistoryParameters struct { + ChannelID string + Cursor string + Inclusive bool + Latest string + Limit int + Oldest string +} + +type GetConversationHistoryResponse struct { + SlackResponse + HasMore bool `json:"has_more"` + PinCount int `json:"pin_count"` + Latest string `json:"latest"` + ResponseMetaData struct { + NextCursor string `json:"next_cursor"` + } `json:"response_metadata"` + Messages []Message `json:"messages"` +} + +// GetConversationHistory joins an existing conversation +func (api *Client) GetConversationHistory(params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) { + return api.GetConversationHistoryContext(context.Background(), params) +} + +// GetConversationHistoryContext joins an existing conversation with a custom context +func (api *Client) GetConversationHistoryContext(ctx context.Context, params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) { + values := url.Values{"token": {api.token}, "channel": {params.ChannelID}} + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + if params.Latest != "" { + values.Add("latest", params.Latest) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Oldest != "" { + values.Add("oldest", params.Oldest) + } + + response := GetConversationHistoryResponse{} + + err := api.postMethod(ctx, "conversations.history", values, &response) + if err != nil { + return nil, err + } + + return &response, response.Err() +} + +// MarkConversation sets the read mark of a conversation to a specific point +func (api *Client) MarkConversation(channel, ts string) (err error) { + return api.MarkConversationContext(context.Background(), channel, ts) +} + +// MarkConversationContext sets the read mark of a conversation to a specific point with a custom context +func (api *Client) MarkConversationContext(ctx context.Context, channel, ts string) error { + values := url.Values{ + "token": {api.token}, + "channel": {channel}, + "ts": {ts}, + } + + response := &SlackResponse{} + + err := api.postMethod(ctx, "conversations.mark", values, response) + if err != nil { + return err + } + return response.Err() +} diff --git a/backend/vendor/github.com/slack-go/slack/dialog.go b/backend/vendor/github.com/slack-go/slack/dialog.go new file mode 100644 index 00000000..f94113f4 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/dialog.go @@ -0,0 +1,120 @@ +package slack + +import ( + "context" + "encoding/json" + "strings" +) + +// InputType is the type of the dialog input type +type InputType string + +const ( + // InputTypeText textfield input + InputTypeText InputType = "text" + // InputTypeTextArea textarea input + InputTypeTextArea InputType = "textarea" + // InputTypeSelect select menus input + InputTypeSelect InputType = "select" +) + +// DialogInput for dialogs input type text or menu +type DialogInput struct { + Type InputType `json:"type"` + Label string `json:"label"` + Name string `json:"name"` + Placeholder string `json:"placeholder"` + Optional bool `json:"optional"` + Hint string `json:"hint"` +} + +// DialogTrigger ... +type DialogTrigger struct { + TriggerID string `json:"trigger_id"` //Required. Must respond within 3 seconds. + Dialog Dialog `json:"dialog"` //Required. +} + +// Dialog as in Slack dialogs +// https://api.slack.com/dialogs#option_element_attributes#top-level_dialog_attributes +type Dialog struct { + TriggerID string `json:"trigger_id"` // Required + CallbackID string `json:"callback_id"` // Required + State string `json:"state,omitempty"` // Optional + Title string `json:"title"` + SubmitLabel string `json:"submit_label,omitempty"` + NotifyOnCancel bool `json:"notify_on_cancel"` + Elements []DialogElement `json:"elements"` +} + +// DialogElement abstract type for dialogs. +type DialogElement interface{} + +// DialogCallback DEPRECATED use InteractionCallback +type DialogCallback InteractionCallback + +// DialogSubmissionCallback is sent from Slack when a user submits a form from within a dialog +type DialogSubmissionCallback struct { + // NOTE: State is only used with the dialog_submission type. + // You should use InteractionCallback.BlockActionsState for block_actions type. + State string `json:"-"` + Submission map[string]string `json:"submission"` +} + +// DialogOpenResponse response from `dialog.open` +type DialogOpenResponse struct { + SlackResponse + DialogResponseMetadata DialogResponseMetadata `json:"response_metadata"` +} + +// DialogResponseMetadata lists the error messages +type DialogResponseMetadata struct { + Messages []string `json:"messages"` +} + +// DialogInputValidationError is an error when user inputs incorrect value to form from within a dialog +type DialogInputValidationError struct { + Name string `json:"name"` + Error string `json:"error"` +} + +// DialogInputValidationErrors lists the name of field and that error messages +type DialogInputValidationErrors struct { + Errors []DialogInputValidationError `json:"errors"` +} + +// OpenDialog opens a dialog window where the triggerID originated from. +// EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable. +func (api *Client) OpenDialog(triggerID string, dialog Dialog) (err error) { + return api.OpenDialogContext(context.Background(), triggerID, dialog) +} + +// OpenDialogContext opens a dialog window where the triggerId originated from with a custom context +// EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable. +func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dialog Dialog) (err error) { + if triggerID == "" { + return ErrParametersMissing + } + + req := DialogTrigger{ + TriggerID: triggerID, + Dialog: dialog, + } + + encoded, err := json.Marshal(req) + if err != nil { + return err + } + + response := &DialogOpenResponse{} + endpoint := api.endpoint + "dialog.open" + if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api); err != nil { + return err + } + + if len(response.DialogResponseMetadata.Messages) > 0 { + response.Ok = false + response.Error += "\n" + strings.Join(response.DialogResponseMetadata.Messages, "\n") + } + + return response.Err() +} diff --git a/backend/vendor/github.com/slack-go/slack/dialog_select.go b/backend/vendor/github.com/slack-go/slack/dialog_select.go new file mode 100644 index 00000000..385cef68 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/dialog_select.go @@ -0,0 +1,101 @@ +package slack + +// SelectDataSource types of select datasource +type SelectDataSource string + +const ( + // DialogDataSourceStatic menu with static Options/OptionGroups + DialogDataSourceStatic SelectDataSource = "static" + // DialogDataSourceExternal dynamic datasource + DialogDataSourceExternal SelectDataSource = "external" + // DialogDataSourceConversations provides a list of conversations + DialogDataSourceConversations SelectDataSource = "conversations" + // DialogDataSourceChannels provides a list of channels + DialogDataSourceChannels SelectDataSource = "channels" + // DialogDataSourceUsers provides a list of users + DialogDataSourceUsers SelectDataSource = "users" +) + +// DialogInputSelect dialog support for select boxes. +type DialogInputSelect struct { + DialogInput + Value string `json:"value,omitempty"` //Optional. + DataSource SelectDataSource `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external". + SelectedOptions []DialogSelectOption `json:"selected_options,omitempty"` //Optional. May hold at most one element, for use with "external" only. + Options []DialogSelectOption `json:"options,omitempty"` //One of options or option_groups is required. + OptionGroups []DialogOptionGroup `json:"option_groups,omitempty"` //Provide up to 100 options. + MinQueryLength int `json:"min_query_length,omitempty"` //Optional. minimum characters before query is sent. + Hint string `json:"hint,omitempty"` //Optional. Additional hint text. +} + +// DialogSelectOption is an option for the user to select from the menu +type DialogSelectOption struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// DialogOptionGroup is a collection of options for creating a segmented table +type DialogOptionGroup struct { + Label string `json:"label"` + Options []DialogSelectOption `json:"options"` +} + +// NewStaticSelectDialogInput constructor for a `static` datasource menu input +func NewStaticSelectDialogInput(name, label string, options []DialogSelectOption) *DialogInputSelect { + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Name: name, + Label: label, + Optional: true, + }, + DataSource: DialogDataSourceStatic, + Options: options, + } +} + +// NewGroupedSelectDialogInput creates grouped options select input for Dialogs. +func NewGroupedSelectDialogInput(name, label string, options []DialogOptionGroup) *DialogInputSelect { + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Name: name, + Label: label, + }, + DataSource: DialogDataSourceStatic, + OptionGroups: options} +} + +// NewDialogOptionGroup creates a DialogOptionGroup from several select options +func NewDialogOptionGroup(label string, options ...DialogSelectOption) DialogOptionGroup { + return DialogOptionGroup{ + Label: label, + Options: options, + } +} + +// NewConversationsSelect returns a `Conversations` select +func NewConversationsSelect(name, label string) *DialogInputSelect { + return newPresetSelect(name, label, DialogDataSourceConversations) +} + +// NewChannelsSelect returns a `Channels` select +func NewChannelsSelect(name, label string) *DialogInputSelect { + return newPresetSelect(name, label, DialogDataSourceChannels) +} + +// NewUsersSelect returns a `Users` select +func NewUsersSelect(name, label string) *DialogInputSelect { + return newPresetSelect(name, label, DialogDataSourceUsers) +} + +func newPresetSelect(name, label string, dataSourceType SelectDataSource) *DialogInputSelect { + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Label: label, + Name: name, + }, + DataSource: dataSourceType, + } +} diff --git a/backend/vendor/github.com/slack-go/slack/dialog_text.go b/backend/vendor/github.com/slack-go/slack/dialog_text.go new file mode 100644 index 00000000..da06bd6d --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/dialog_text.go @@ -0,0 +1,59 @@ +package slack + +// TextInputSubtype Accepts email, number, tel, or url. In some form factors, optimized input is provided for this subtype. +type TextInputSubtype string + +// TextInputOption handle to extra inputs options. +type TextInputOption func(*TextInputElement) + +const ( + // InputSubtypeEmail email keyboard + InputSubtypeEmail TextInputSubtype = "email" + // InputSubtypeNumber numeric keyboard + InputSubtypeNumber TextInputSubtype = "number" + // InputSubtypeTel Phone keyboard + InputSubtypeTel TextInputSubtype = "tel" + // InputSubtypeURL Phone keyboard + InputSubtypeURL TextInputSubtype = "url" +) + +// TextInputElement subtype of DialogInput +// https://api.slack.com/dialogs#option_element_attributes#text_element_attributes +type TextInputElement struct { + DialogInput + MaxLength int `json:"max_length,omitempty"` + MinLength int `json:"min_length,omitempty"` + Hint string `json:"hint,omitempty"` + Subtype TextInputSubtype `json:"subtype"` + Value string `json:"value"` +} + +// NewTextInput constructor for a `text` input +func NewTextInput(name, label, text string, options ...TextInputOption) *TextInputElement { + t := &TextInputElement{ + DialogInput: DialogInput{ + Type: InputTypeText, + Name: name, + Label: label, + }, + Value: text, + } + + for _, opt := range options { + opt(t) + } + + return t +} + +// NewTextAreaInput constructor for a `textarea` input +func NewTextAreaInput(name, label, text string) *TextInputElement { + return &TextInputElement{ + DialogInput: DialogInput{ + Type: InputTypeTextArea, + Name: name, + Label: label, + }, + Value: text, + } +} diff --git a/backend/vendor/github.com/slack-go/slack/dnd.go b/backend/vendor/github.com/slack-go/slack/dnd.go new file mode 100644 index 00000000..a3aa680c --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/dnd.go @@ -0,0 +1,151 @@ +package slack + +import ( + "context" + "net/url" + "strconv" + "strings" +) + +type SnoozeDebug struct { + SnoozeEndDate string `json:"snooze_end_date"` +} + +type SnoozeInfo struct { + SnoozeEnabled bool `json:"snooze_enabled,omitempty"` + SnoozeEndTime int `json:"snooze_endtime,omitempty"` + SnoozeRemaining int `json:"snooze_remaining,omitempty"` + SnoozeDebug SnoozeDebug `json:"snooze_debug,omitempty"` +} + +type DNDStatus struct { + Enabled bool `json:"dnd_enabled"` + NextStartTimestamp int `json:"next_dnd_start_ts"` + NextEndTimestamp int `json:"next_dnd_end_ts"` + SnoozeInfo +} + +type dndResponseFull struct { + DNDStatus + SlackResponse +} + +type dndTeamInfoResponse struct { + Users map[string]DNDStatus `json:"users"` + SlackResponse +} + +func (api *Client) dndRequest(ctx context.Context, path string, values url.Values) (*dndResponseFull, error) { + response := &dndResponseFull{} + err := api.postMethod(ctx, path, values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// EndDND ends the user's scheduled Do Not Disturb session +func (api *Client) EndDND() error { + return api.EndDNDContext(context.Background()) +} + +// EndDNDContext ends the user's scheduled Do Not Disturb session with a custom context +func (api *Client) EndDNDContext(ctx context.Context) error { + values := url.Values{ + "token": {api.token}, + } + + response := &SlackResponse{} + + if err := api.postMethod(ctx, "dnd.endDnd", values, response); err != nil { + return err + } + + return response.Err() +} + +// EndSnooze ends the current user's snooze mode +func (api *Client) EndSnooze() (*DNDStatus, error) { + return api.EndSnoozeContext(context.Background()) +} + +// EndSnoozeContext ends the current user's snooze mode with a custom context +func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { + values := url.Values{ + "token": {api.token}, + } + + response, err := api.dndRequest(ctx, "dnd.endSnooze", values) + if err != nil { + return nil, err + } + return &response.DNDStatus, nil +} + +// GetDNDInfo provides information about a user's current Do Not Disturb settings. +func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) { + return api.GetDNDInfoContext(context.Background(), user) +} + +// GetDNDInfoContext provides information about a user's current Do Not Disturb settings with a custom context. +func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDStatus, error) { + values := url.Values{ + "token": {api.token}, + } + if user != nil { + values.Set("user", *user) + } + + response, err := api.dndRequest(ctx, "dnd.info", values) + if err != nil { + return nil, err + } + return &response.DNDStatus, nil +} + +// GetDNDTeamInfo provides information about a user's current Do Not Disturb settings. +func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) { + return api.GetDNDTeamInfoContext(context.Background(), users) +} + +// GetDNDTeamInfoContext provides information about a user's current Do Not Disturb settings with a custom context. +func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (map[string]DNDStatus, error) { + values := url.Values{ + "token": {api.token}, + "users": {strings.Join(users, ",")}, + } + response := &dndTeamInfoResponse{} + + if err := api.postMethod(ctx, "dnd.teamInfo", values, response); err != nil { + return nil, err + } + + if response.Err() != nil { + return nil, response.Err() + } + + return response.Users, nil +} + +// SetSnooze adjusts the snooze duration for a user's Do Not Disturb +// settings. If a snooze session is not already active for the user, invoking +// this method will begin one for the specified duration. +func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) { + return api.SetSnoozeContext(context.Background(), minutes) +} + +// SetSnoozeContext adjusts the snooze duration for a user's Do Not Disturb settings with a custom context. +// For more information see the SetSnooze docs +func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) { + values := url.Values{ + "token": {api.token}, + "num_minutes": {strconv.Itoa(minutes)}, + } + + response, err := api.dndRequest(ctx, "dnd.setSnooze", values) + if err != nil { + return nil, err + } + return &response.DNDStatus, nil +} diff --git a/backend/vendor/github.com/slack-go/slack/emoji.go b/backend/vendor/github.com/slack-go/slack/emoji.go new file mode 100644 index 00000000..b2b0c6c9 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/emoji.go @@ -0,0 +1,35 @@ +package slack + +import ( + "context" + "net/url" +) + +type emojiResponseFull struct { + Emoji map[string]string `json:"emoji"` + SlackResponse +} + +// GetEmoji retrieves all the emojis +func (api *Client) GetEmoji() (map[string]string, error) { + return api.GetEmojiContext(context.Background()) +} + +// GetEmojiContext retrieves all the emojis with a custom context +func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, error) { + values := url.Values{ + "token": {api.token}, + } + response := &emojiResponseFull{} + + err := api.postMethod(ctx, "emoji.list", values, response) + if err != nil { + return nil, err + } + + if response.Err() != nil { + return nil, response.Err() + } + + return response.Emoji, nil +} diff --git a/backend/vendor/github.com/slack-go/slack/errors.go b/backend/vendor/github.com/slack-go/slack/errors.go new file mode 100644 index 00000000..8be22a65 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/errors.go @@ -0,0 +1,21 @@ +package slack + +import "github.com/slack-go/slack/internal/errorsx" + +// Errors returned by various methods. +const ( + ErrAlreadyDisconnected = errorsx.String("Invalid call to Disconnect - Slack API is already disconnected") + ErrRTMDisconnected = errorsx.String("disconnect received while trying to connect") + ErrRTMGoodbye = errorsx.String("goodbye detected") + ErrRTMDeadman = errorsx.String("deadman switch triggered") + ErrParametersMissing = errorsx.String("received empty parameters") + ErrBlockIDNotUnique = errorsx.String("Block ID needs to be unique") + ErrInvalidConfiguration = errorsx.String("invalid configuration") + ErrMissingHeaders = errorsx.String("missing headers") + ErrExpiredTimestamp = errorsx.String("timestamp is too old") +) + +// internal errors +const ( + errPaginationComplete = errorsx.String("pagination complete") +) diff --git a/backend/vendor/github.com/slack-go/slack/files.go b/backend/vendor/github.com/slack-go/slack/files.go new file mode 100644 index 00000000..713038f0 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/files.go @@ -0,0 +1,410 @@ +package slack + +import ( + "context" + "fmt" + "io" + "net/url" + "strconv" + "strings" +) + +const ( + // Add here the defaults in the siten + DEFAULT_FILES_USER = "" + DEFAULT_FILES_CHANNEL = "" + DEFAULT_FILES_TS_FROM = 0 + DEFAULT_FILES_TS_TO = -1 + DEFAULT_FILES_TYPES = "all" + DEFAULT_FILES_COUNT = 100 + DEFAULT_FILES_PAGE = 1 + DEFAULT_FILES_SHOW_HIDDEN = false +) + +// File contains all the information for a file +type File struct { + ID string `json:"id"` + Created JSONTime `json:"created"` + Timestamp JSONTime `json:"timestamp"` + + Name string `json:"name"` + Title string `json:"title"` + Mimetype string `json:"mimetype"` + ImageExifRotation int `json:"image_exif_rotation"` + Filetype string `json:"filetype"` + PrettyType string `json:"pretty_type"` + User string `json:"user"` + + Mode string `json:"mode"` + Editable bool `json:"editable"` + IsExternal bool `json:"is_external"` + ExternalType string `json:"external_type"` + + Size int `json:"size"` + + URL string `json:"url"` // Deprecated - never set + URLDownload string `json:"url_download"` // Deprecated - never set + URLPrivate string `json:"url_private"` + URLPrivateDownload string `json:"url_private_download"` + + OriginalH int `json:"original_h"` + OriginalW int `json:"original_w"` + Thumb64 string `json:"thumb_64"` + Thumb80 string `json:"thumb_80"` + Thumb160 string `json:"thumb_160"` + Thumb360 string `json:"thumb_360"` + Thumb360Gif string `json:"thumb_360_gif"` + Thumb360W int `json:"thumb_360_w"` + Thumb360H int `json:"thumb_360_h"` + Thumb480 string `json:"thumb_480"` + Thumb480W int `json:"thumb_480_w"` + Thumb480H int `json:"thumb_480_h"` + Thumb720 string `json:"thumb_720"` + Thumb720W int `json:"thumb_720_w"` + Thumb720H int `json:"thumb_720_h"` + Thumb960 string `json:"thumb_960"` + Thumb960W int `json:"thumb_960_w"` + Thumb960H int `json:"thumb_960_h"` + Thumb1024 string `json:"thumb_1024"` + Thumb1024W int `json:"thumb_1024_w"` + Thumb1024H int `json:"thumb_1024_h"` + + Permalink string `json:"permalink"` + PermalinkPublic string `json:"permalink_public"` + + EditLink string `json:"edit_link"` + Preview string `json:"preview"` + PreviewHighlight string `json:"preview_highlight"` + Lines int `json:"lines"` + LinesMore int `json:"lines_more"` + + IsPublic bool `json:"is_public"` + PublicURLShared bool `json:"public_url_shared"` + Channels []string `json:"channels"` + Groups []string `json:"groups"` + IMs []string `json:"ims"` + InitialComment Comment `json:"initial_comment"` + CommentsCount int `json:"comments_count"` + NumStars int `json:"num_stars"` + IsStarred bool `json:"is_starred"` + Shares Share `json:"shares"` +} + +type Share struct { + Public map[string][]ShareFileInfo `json:"public"` + Private map[string][]ShareFileInfo `json:"private"` +} + +type ShareFileInfo struct { + ReplyUsers []string `json:"reply_users"` + ReplyUsersCount int `json:"reply_users_count"` + ReplyCount int `json:"reply_count"` + Ts string `json:"ts"` + ThreadTs string `json:"thread_ts"` + LatestReply string `json:"latest_reply"` + ChannelName string `json:"channel_name"` + TeamID string `json:"team_id"` +} + +// FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request. +// +// There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large, +// or provide a local file path in File to upload it from your filesystem. +// +// Note that when using the Reader option, you *must* specify the Filename, otherwise the Slack API isn't happy. +type FileUploadParameters struct { + File string + Content string + Reader io.Reader + Filetype string + Filename string + Title string + InitialComment string + Channels []string + ThreadTimestamp string +} + +// GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request +type GetFilesParameters struct { + User string + Channel string + TimestampFrom JSONTime + TimestampTo JSONTime + Types string + Count int + Page int + ShowHidden bool +} + +// ListFilesParameters contains all the parameters necessary (including the optional ones) for a ListFiles() request +type ListFilesParameters struct { + Limit int + User string + Channel string + Types string + Cursor string +} + +type fileResponseFull struct { + File `json:"file"` + Paging `json:"paging"` + Comments []Comment `json:"comments"` + Files []File `json:"files"` + Metadata ResponseMetadata `json:"response_metadata"` + + SlackResponse +} + +// NewGetFilesParameters provides an instance of GetFilesParameters with all the sane default values set +func NewGetFilesParameters() GetFilesParameters { + return GetFilesParameters{ + User: DEFAULT_FILES_USER, + Channel: DEFAULT_FILES_CHANNEL, + TimestampFrom: DEFAULT_FILES_TS_FROM, + TimestampTo: DEFAULT_FILES_TS_TO, + Types: DEFAULT_FILES_TYPES, + Count: DEFAULT_FILES_COUNT, + Page: DEFAULT_FILES_PAGE, + ShowHidden: DEFAULT_FILES_SHOW_HIDDEN, + } +} + +func (api *Client) fileRequest(ctx context.Context, path string, values url.Values) (*fileResponseFull, error) { + response := &fileResponseFull{} + err := api.postMethod(ctx, path, values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// GetFileInfo retrieves a file and related comments +func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment, *Paging, error) { + return api.GetFileInfoContext(context.Background(), fileID, count, page) +} + +// GetFileInfoContext retrieves a file and related comments with a custom context +func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*File, []Comment, *Paging, error) { + values := url.Values{ + "token": {api.token}, + "file": {fileID}, + "count": {strconv.Itoa(count)}, + "page": {strconv.Itoa(page)}, + } + + response, err := api.fileRequest(ctx, "files.info", values) + if err != nil { + return nil, nil, nil, err + } + return &response.File, response.Comments, &response.Paging, nil +} + +// GetFile retreives a given file from its private download URL +func (api *Client) GetFile(downloadURL string, writer io.Writer) error { + return downloadFile(api.httpclient, api.token, downloadURL, writer, api) +} + +// GetFiles retrieves all files according to the parameters given +func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) { + return api.GetFilesContext(context.Background(), params) +} + +// ListFiles retrieves all files according to the parameters given. Uses cursor based pagination. +func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) { + return api.ListFilesContext(context.Background(), params) +} + +// ListFilesContext retrieves all files according to the parameters given with a custom context. Uses cursor based pagination. +func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) { + values := url.Values{ + "token": {api.token}, + } + + if params.User != DEFAULT_FILES_USER { + values.Add("user", params.User) + } + if params.Channel != DEFAULT_FILES_CHANNEL { + values.Add("channel", params.Channel) + } + if params.Limit != DEFAULT_FILES_COUNT { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + + response, err := api.fileRequest(ctx, "files.list", values) + if err != nil { + return nil, nil, err + } + + params.Cursor = response.Metadata.Cursor + + return response.Files, ¶ms, nil +} + +// GetFilesContext retrieves all files according to the parameters given with a custom context +func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) { + values := url.Values{ + "token": {api.token}, + } + if params.User != DEFAULT_FILES_USER { + values.Add("user", params.User) + } + if params.Channel != DEFAULT_FILES_CHANNEL { + values.Add("channel", params.Channel) + } + if params.TimestampFrom != DEFAULT_FILES_TS_FROM { + values.Add("ts_from", strconv.FormatInt(int64(params.TimestampFrom), 10)) + } + if params.TimestampTo != DEFAULT_FILES_TS_TO { + values.Add("ts_to", strconv.FormatInt(int64(params.TimestampTo), 10)) + } + if params.Types != DEFAULT_FILES_TYPES { + values.Add("types", params.Types) + } + if params.Count != DEFAULT_FILES_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Page != DEFAULT_FILES_PAGE { + values.Add("page", strconv.Itoa(params.Page)) + } + if params.ShowHidden != DEFAULT_FILES_SHOW_HIDDEN { + values.Add("show_files_hidden_by_limit", strconv.FormatBool(params.ShowHidden)) + } + + response, err := api.fileRequest(ctx, "files.list", values) + if err != nil { + return nil, nil, err + } + return response.Files, &response.Paging, nil +} + +// UploadFile uploads a file +func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { + return api.UploadFileContext(context.Background(), params) +} + +// UploadFileContext uploads a file and setting a custom context +func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParameters) (file *File, err error) { + // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More + // investigation needed, but for now this will do. + _, err = api.AuthTest() + if err != nil { + return nil, err + } + response := &fileResponseFull{} + values := url.Values{ + "token": {api.token}, + } + if params.Filetype != "" { + values.Add("filetype", params.Filetype) + } + if params.Filename != "" { + values.Add("filename", params.Filename) + } + if params.Title != "" { + values.Add("title", params.Title) + } + if params.InitialComment != "" { + values.Add("initial_comment", params.InitialComment) + } + if params.ThreadTimestamp != "" { + values.Add("thread_ts", params.ThreadTimestamp) + } + if len(params.Channels) != 0 { + values.Add("channels", strings.Join(params.Channels, ",")) + } + if params.Content != "" { + values.Add("content", params.Content) + err = api.postMethod(ctx, "files.upload", values, response) + } else if params.File != "" { + err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", values, response, api) + } else if params.Reader != nil { + if params.Filename == "" { + return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory when using FileUploadParameters.Reader") + } + err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", values, params.Reader, response, api) + } + + if err != nil { + return nil, err + } + + return &response.File, response.Err() +} + +// DeleteFileComment deletes a file's comment +func (api *Client) DeleteFileComment(commentID, fileID string) error { + return api.DeleteFileCommentContext(context.Background(), fileID, commentID) +} + +// DeleteFileCommentContext deletes a file's comment with a custom context +func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, commentID string) (err error) { + if fileID == "" || commentID == "" { + return ErrParametersMissing + } + + values := url.Values{ + "token": {api.token}, + "file": {fileID}, + "id": {commentID}, + } + _, err = api.fileRequest(ctx, "files.comments.delete", values) + return err +} + +// DeleteFile deletes a file +func (api *Client) DeleteFile(fileID string) error { + return api.DeleteFileContext(context.Background(), fileID) +} + +// DeleteFileContext deletes a file with a custom context +func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err error) { + values := url.Values{ + "token": {api.token}, + "file": {fileID}, + } + + _, err = api.fileRequest(ctx, "files.delete", values) + return err +} + +// RevokeFilePublicURL disables public/external sharing for a file +func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) { + return api.RevokeFilePublicURLContext(context.Background(), fileID) +} + +// RevokeFilePublicURLContext disables public/external sharing for a file with a custom context +func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string) (*File, error) { + values := url.Values{ + "token": {api.token}, + "file": {fileID}, + } + + response, err := api.fileRequest(ctx, "files.revokePublicURL", values) + if err != nil { + return nil, err + } + return &response.File, nil +} + +// ShareFilePublicURL enabled public/external sharing for a file +func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging, error) { + return api.ShareFilePublicURLContext(context.Background(), fileID) +} + +// ShareFilePublicURLContext enabled public/external sharing for a file with a custom context +func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) (*File, []Comment, *Paging, error) { + values := url.Values{ + "token": {api.token}, + "file": {fileID}, + } + + response, err := api.fileRequest(ctx, "files.sharedPublicURL", values) + if err != nil { + return nil, nil, nil, err + } + return &response.File, response.Comments, &response.Paging, nil +} diff --git a/backend/vendor/github.com/slack-go/slack/go.mod b/backend/vendor/github.com/slack-go/slack/go.mod new file mode 100644 index 00000000..32fc4b93 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/go.mod @@ -0,0 +1,12 @@ +module github.com/slack-go/slack + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-test/deep v1.0.4 + github.com/gorilla/websocket v1.4.2 + github.com/pkg/errors v0.8.0 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.2.2 +) + +go 1.13 diff --git a/backend/vendor/github.com/slack-go/slack/go.sum b/backend/vendor/github.com/slack-go/slack/go.sum new file mode 100644 index 00000000..d01bacbf --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/backend/vendor/github.com/slack-go/slack/groups.go b/backend/vendor/github.com/slack-go/slack/groups.go new file mode 100644 index 00000000..4c5793dc --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/groups.go @@ -0,0 +1,574 @@ +package slack + +import ( + "context" + "net/url" + "strconv" +) + +// Group contains all the information for a group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +type Group struct { + GroupConversation + IsGroup bool `json:"is_group"` +} + +type groupResponseFull struct { + Group Group `json:"group"` + Groups []Group `json:"groups"` + Purpose string `json:"purpose"` + Topic string `json:"topic"` + NotInGroup bool `json:"not_in_group"` + NoOp bool `json:"no_op"` + AlreadyClosed bool `json:"already_closed"` + AlreadyOpen bool `json:"already_open"` + AlreadyInGroup bool `json:"already_in_group"` + Channel Channel `json:"channel"` + History + SlackResponse +} + +func (api *Client) groupRequest(ctx context.Context, path string, values url.Values) (*groupResponseFull, error) { + response := &groupResponseFull{} + err := api.postMethod(ctx, path, values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// ArchiveGroup archives a private group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) ArchiveGroup(group string) error { + return api.ArchiveGroupContext(context.Background(), group) +} + +// ArchiveGroupContext archives a private group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + } + + _, err := api.groupRequest(ctx, "groups.archive", values) + return err +} + +// UnarchiveGroup unarchives a private group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) UnarchiveGroup(group string) error { + return api.UnarchiveGroupContext(context.Background(), group) +} + +// UnarchiveGroupContext unarchives a private group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) error { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + } + + _, err := api.groupRequest(ctx, "groups.unarchive", values) + return err +} + +// CreateGroup creates a private group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) CreateGroup(group string) (*Group, error) { + return api.CreateGroupContext(context.Background(), group) +} + +// CreateGroupContext creates a private group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group, error) { + values := url.Values{ + "token": {api.token}, + "name": {group}, + } + + response, err := api.groupRequest(ctx, "groups.create", values) + if err != nil { + return nil, err + } + return &response.Group, nil +} + +// CreateChildGroup creates a new private group archiving the old one +// This method takes an existing private group and performs the following steps: +// 1. Renames the existing group (from "example" to "example-archived"). +// 2. Archives the existing group. +// 3. Creates a new group with the name of the existing group. +// 4. Adds all members of the existing group to the new group. +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) CreateChildGroup(group string) (*Group, error) { + return api.CreateChildGroupContext(context.Background(), group) +} + +// CreateChildGroupContext creates a new private group archiving the old one with a custom context +// For more information see CreateChildGroup +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (*Group, error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + } + + response, err := api.groupRequest(ctx, "groups.createChild", values) + if err != nil { + return nil, err + } + return &response.Group, nil +} + +// GetGroupHistory fetches all the history for a private group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) { + return api.GetGroupHistoryContext(context.Background(), group, params) +} + +// GetGroupHistoryContext fetches all the history for a private group with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, params HistoryParameters) (*History, error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + } + if params.Latest != DEFAULT_HISTORY_LATEST { + values.Add("latest", params.Latest) + } + if params.Oldest != DEFAULT_HISTORY_OLDEST { + values.Add("oldest", params.Oldest) + } + if params.Count != DEFAULT_HISTORY_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + } + if params.Unreads != DEFAULT_HISTORY_UNREADS { + if params.Unreads { + values.Add("unreads", "1") + } else { + values.Add("unreads", "0") + } + } + + response, err := api.groupRequest(ctx, "groups.history", values) + if err != nil { + return nil, err + } + return &response.History, nil +} + +// InviteUserToGroup invites a specific user to a private group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) { + return api.InviteUserToGroupContext(context.Background(), group, user) +} + +// InviteUserToGroupContext invites a specific user to a private group with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user string) (*Group, bool, error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + "user": {user}, + } + + response, err := api.groupRequest(ctx, "groups.invite", values) + if err != nil { + return nil, false, err + } + return &response.Group, response.AlreadyInGroup, nil +} + +// LeaveGroup makes authenticated user leave the group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) LeaveGroup(group string) error { + return api.LeaveGroupContext(context.Background(), group) +} + +// LeaveGroupContext makes authenticated user leave the group with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + } + + _, err = api.groupRequest(ctx, "groups.leave", values) + return err +} + +// KickUserFromGroup kicks a user from a group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) KickUserFromGroup(group, user string) error { + return api.KickUserFromGroupContext(context.Background(), group, user) +} + +// KickUserFromGroupContext kicks a user from a group with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user string) (err error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + "user": {user}, + } + + _, err = api.groupRequest(ctx, "groups.kick", values) + return err +} + +// GetGroups retrieves all groups +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) { + return api.GetGroupsContext(context.Background(), excludeArchived) +} + +// GetGroupsContext retrieves all groups with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) ([]Group, error) { + values := url.Values{ + "token": {api.token}, + } + if excludeArchived { + values.Add("exclude_archived", "1") + } + + response, err := api.groupRequest(ctx, "groups.list", values) + if err != nil { + return nil, err + } + return response.Groups, nil +} + +// GetGroupInfo retrieves the given group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetGroupInfo(group string) (*Group, error) { + return api.GetGroupInfoContext(context.Background(), group) +} + +// GetGroupInfoContext retrieves the given group with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + "include_locale": {strconv.FormatBool(true)}, + } + + response, err := api.groupRequest(ctx, "groups.info", values) + if err != nil { + return nil, err + } + return &response.Group, nil +} + +// SetGroupReadMark sets the read mark on a private group +// Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a +// timer before making the call. In this way, any further updates needed during the timeout will not generate extra +// calls (just one per channel). This is useful for when reading scroll-back history, or following a busy live +// channel. A timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) SetGroupReadMark(group, ts string) error { + return api.SetGroupReadMarkContext(context.Background(), group, ts) +} + +// SetGroupReadMarkContext sets the read mark on a private group with a custom context +// For more details see SetGroupReadMark +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string) (err error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + "ts": {ts}, + } + + _, err = api.groupRequest(ctx, "groups.mark", values) + return err +} + +// OpenGroup opens a private group +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) OpenGroup(group string) (bool, bool, error) { + return api.OpenGroupContext(context.Background(), group) +} + +// OpenGroupContext opens a private group with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bool, error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + } + + response, err := api.groupRequest(ctx, "groups.open", values) + if err != nil { + return false, false, err + } + return response.NoOp, response.AlreadyOpen, nil +} + +// RenameGroup renames a group +// XXX: They return a channel, not a group. What is this crap? :( +// Inconsistent api it seems. +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) RenameGroup(group, name string) (*Channel, error) { + return api.RenameGroupContext(context.Background(), group, name) +} + +// RenameGroupContext renames a group with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) RenameGroupContext(ctx context.Context, group, name string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + "name": {name}, + } + + // XXX: the created entry in this call returns a string instead of a number + // so I may have to do some workaround to solve it. + response, err := api.groupRequest(ctx, "groups.rename", values) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// SetGroupPurpose sets the group purpose +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) SetGroupPurpose(group, purpose string) (string, error) { + return api.SetGroupPurposeContext(context.Background(), group, purpose) +} + +// SetGroupPurposeContext sets the group purpose with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose string) (string, error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + "purpose": {purpose}, + } + + response, err := api.groupRequest(ctx, "groups.setPurpose", values) + if err != nil { + return "", err + } + return response.Purpose, nil +} + +// SetGroupTopic sets the group topic +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) SetGroupTopic(group, topic string) (string, error) { + return api.SetGroupTopicContext(context.Background(), group, topic) +} + +// SetGroupTopicContext sets the group topic with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string) (string, error) { + values := url.Values{ + "token": {api.token}, + "channel": {group}, + "topic": {topic}, + } + + response, err := api.groupRequest(ctx, "groups.setTopic", values) + if err != nil { + return "", err + } + return response.Topic, nil +} + +// GetGroupReplies gets an entire thread (a message plus all the messages in reply to it). +// see https://api.slack.com/methods/groups.replies +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetGroupReplies(channelID, thread_ts string) ([]Message, error) { + return api.GetGroupRepliesContext(context.Background(), channelID, thread_ts) +} + +// GetGroupRepliesContext gets an entire thread (a message plus all the messages in reply to it) with a custom context +// see https://api.slack.com/methods/groups.replies +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetGroupRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "thread_ts": {thread_ts}, + } + response, err := api.groupRequest(ctx, "groups.replies", values) + if err != nil { + return nil, err + } + return response.History.Messages, nil +} diff --git a/backend/vendor/github.com/slack-go/slack/history.go b/backend/vendor/github.com/slack-go/slack/history.go new file mode 100644 index 00000000..49dfe354 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/history.go @@ -0,0 +1,37 @@ +package slack + +const ( + DEFAULT_HISTORY_LATEST = "" + DEFAULT_HISTORY_OLDEST = "0" + DEFAULT_HISTORY_COUNT = 100 + DEFAULT_HISTORY_INCLUSIVE = false + DEFAULT_HISTORY_UNREADS = false +) + +// HistoryParameters contains all the necessary information to help in the retrieval of history for Channels/Groups/DMs +type HistoryParameters struct { + Latest string + Oldest string + Count int + Inclusive bool + Unreads bool +} + +// History contains message history information needed to navigate a Channel / Group / DM history +type History struct { + Latest string `json:"latest"` + Messages []Message `json:"messages"` + HasMore bool `json:"has_more"` + Unread int `json:"unread_count_display"` +} + +// NewHistoryParameters provides an instance of HistoryParameters with all the sane default values set +func NewHistoryParameters() HistoryParameters { + return HistoryParameters{ + Latest: DEFAULT_HISTORY_LATEST, + Oldest: DEFAULT_HISTORY_OLDEST, + Count: DEFAULT_HISTORY_COUNT, + Inclusive: DEFAULT_HISTORY_INCLUSIVE, + Unreads: DEFAULT_HISTORY_UNREADS, + } +} diff --git a/backend/vendor/github.com/slack-go/slack/im.go b/backend/vendor/github.com/slack-go/slack/im.go new file mode 100644 index 00000000..a884b295 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/im.go @@ -0,0 +1,220 @@ +package slack + +import ( + "context" + "net/url" + "strconv" +) + +type imChannel struct { + ID string `json:"id"` +} + +type imResponseFull struct { + NoOp bool `json:"no_op"` + AlreadyClosed bool `json:"already_closed"` + AlreadyOpen bool `json:"already_open"` + Channel imChannel `json:"channel"` + IMs []IM `json:"ims"` + History + SlackResponse +} + +// IM contains information related to the Direct Message channel +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +type IM struct { + Conversation + IsUserDeleted bool `json:"is_user_deleted"` +} + +func (api *Client) imRequest(ctx context.Context, path string, values url.Values) (*imResponseFull, error) { + response := &imResponseFull{} + err := api.postMethod(ctx, path, values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// CloseIMChannel closes the direct message channel +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) CloseIMChannel(channel string) (bool, bool, error) { + return api.CloseIMChannelContext(context.Background(), channel) +} + +// CloseIMChannelContext closes the direct message channel with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (bool, bool, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channel}, + } + + response, err := api.imRequest(ctx, "im.close", values) + if err != nil { + return false, false, err + } + return response.NoOp, response.AlreadyClosed, nil +} + +// OpenIMChannel opens a direct message channel to the user provided as argument +// Returns some status and the channel ID +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) { + return api.OpenIMChannelContext(context.Background(), user) +} + +// OpenIMChannelContext opens a direct message channel to the user provided as argument with a custom context +// Returns some status and the channel ID +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool, bool, string, error) { + values := url.Values{ + "token": {api.token}, + "user": {user}, + } + + response, err := api.imRequest(ctx, "im.open", values) + if err != nil { + return false, false, "", err + } + return response.NoOp, response.AlreadyOpen, response.Channel.ID, nil +} + +// MarkIMChannel sets the read mark of a direct message channel to a specific point +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) MarkIMChannel(channel, ts string) (err error) { + return api.MarkIMChannelContext(context.Background(), channel, ts) +} + +// MarkIMChannelContext sets the read mark of a direct message channel to a specific point with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) error { + values := url.Values{ + "token": {api.token}, + "channel": {channel}, + "ts": {ts}, + } + + _, err := api.imRequest(ctx, "im.mark", values) + return err +} + +// GetIMHistory retrieves the direct message channel history +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*History, error) { + return api.GetIMHistoryContext(context.Background(), channel, params) +} + +// GetIMHistoryContext retrieves the direct message channel history with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channel}, + } + if params.Latest != DEFAULT_HISTORY_LATEST { + values.Add("latest", params.Latest) + } + if params.Oldest != DEFAULT_HISTORY_OLDEST { + values.Add("oldest", params.Oldest) + } + if params.Count != DEFAULT_HISTORY_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + } + if params.Unreads != DEFAULT_HISTORY_UNREADS { + if params.Unreads { + values.Add("unreads", "1") + } else { + values.Add("unreads", "0") + } + } + + response, err := api.imRequest(ctx, "im.history", values) + if err != nil { + return nil, err + } + return &response.History, nil +} + +// GetIMChannels returns the list of direct message channels +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetIMChannels() ([]IM, error) { + return api.GetIMChannelsContext(context.Background()) +} + +// GetIMChannelsContext returns the list of direct message channels with a custom context +// +// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. +// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. +// Also, existing applications will not be able to use these APIs after February 24th, 2021. +// +// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api +func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) { + values := url.Values{ + "token": {api.token}, + } + + response, err := api.imRequest(ctx, "im.list", values) + if err != nil { + return nil, err + } + return response.IMs, nil +} diff --git a/backend/vendor/github.com/slack-go/slack/info.go b/backend/vendor/github.com/slack-go/slack/info.go new file mode 100644 index 00000000..ec706240 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/info.go @@ -0,0 +1,468 @@ +package slack + +import ( + "bytes" + "context" + "fmt" + "net/url" + "strconv" + "strings" + "time" +) + +type UserPrefsCarrier struct { + SlackResponse + UserPrefs *UserPrefs `json:"prefs"` +} + +// UserPrefs carries a bunch of user settings including some unknown types +type UserPrefs struct { + UserColors string `json:"user_colors,omitempty"` + ColorNamesInList bool `json:"color_names_in_list,omitempty"` + // Keyboard UnknownType `json:"keyboard"` + EmailAlerts string `json:"email_alerts,omitempty"` + EmailAlertsSleepUntil int `json:"email_alerts_sleep_until,omitempty"` + EmailTips bool `json:"email_tips,omitempty"` + EmailWeekly bool `json:"email_weekly,omitempty"` + EmailOffers bool `json:"email_offers,omitempty"` + EmailResearch bool `json:"email_research,omitempty"` + EmailDeveloper bool `json:"email_developer,omitempty"` + WelcomeMessageHidden bool `json:"welcome_message_hidden,omitempty"` + SearchSort string `json:"search_sort,omitempty"` + SearchFileSort string `json:"search_file_sort,omitempty"` + SearchChannelSort string `json:"search_channel_sort,omitempty"` + SearchPeopleSort string `json:"search_people_sort,omitempty"` + ExpandInlineImages bool `json:"expand_inline_images,omitempty"` + ExpandInternalInlineImages bool `json:"expand_internal_inline_images,omitempty"` + ExpandSnippets bool `json:"expand_snippets,omitempty"` + PostsFormattingGuide bool `json:"posts_formatting_guide,omitempty"` + SeenWelcome2 bool `json:"seen_welcome_2,omitempty"` + SeenSSBPrompt bool `json:"seen_ssb_prompt,omitempty"` + SpacesNewXpBannerDismissed bool `json:"spaces_new_xp_banner_dismissed,omitempty"` + SearchOnlyMyChannels bool `json:"search_only_my_channels,omitempty"` + SearchOnlyCurrentTeam bool `json:"search_only_current_team,omitempty"` + SearchHideMyChannels bool `json:"search_hide_my_channels,omitempty"` + SearchOnlyShowOnline bool `json:"search_only_show_online,omitempty"` + SearchHideDeactivatedUsers bool `json:"search_hide_deactivated_users,omitempty"` + EmojiMode string `json:"emoji_mode,omitempty"` + EmojiUse string `json:"emoji_use,omitempty"` + HasInvited bool `json:"has_invited,omitempty"` + HasUploaded bool `json:"has_uploaded,omitempty"` + HasCreatedChannel bool `json:"has_created_channel,omitempty"` + HasSearched bool `json:"has_searched,omitempty"` + SearchExcludeChannels string `json:"search_exclude_channels,omitempty"` + MessagesTheme string `json:"messages_theme,omitempty"` + WebappSpellcheck bool `json:"webapp_spellcheck,omitempty"` + NoJoinedOverlays bool `json:"no_joined_overlays,omitempty"` + NoCreatedOverlays bool `json:"no_created_overlays,omitempty"` + DropboxEnabled bool `json:"dropbox_enabled,omitempty"` + SeenDomainInviteReminder bool `json:"seen_domain_invite_reminder,omitempty"` + SeenMemberInviteReminder bool `json:"seen_member_invite_reminder,omitempty"` + MuteSounds bool `json:"mute_sounds,omitempty"` + ArrowHistory bool `json:"arrow_history,omitempty"` + TabUIReturnSelects bool `json:"tab_ui_return_selects,omitempty"` + ObeyInlineImgLimit bool `json:"obey_inline_img_limit,omitempty"` + RequireAt bool `json:"require_at,omitempty"` + SsbSpaceWindow string `json:"ssb_space_window,omitempty"` + MacSsbBounce string `json:"mac_ssb_bounce,omitempty"` + MacSsbBullet bool `json:"mac_ssb_bullet,omitempty"` + ExpandNonMediaAttachments bool `json:"expand_non_media_attachments,omitempty"` + ShowTyping bool `json:"show_typing,omitempty"` + PagekeysHandled bool `json:"pagekeys_handled,omitempty"` + LastSnippetType string `json:"last_snippet_type,omitempty"` + DisplayRealNamesOverride int `json:"display_real_names_override,omitempty"` + DisplayDisplayNames bool `json:"display_display_names,omitempty"` + Time24 bool `json:"time24,omitempty"` + EnterIsSpecialInTbt bool `json:"enter_is_special_in_tbt,omitempty"` + MsgInputSendBtn bool `json:"msg_input_send_btn,omitempty"` + MsgInputSendBtnAutoSet bool `json:"msg_input_send_btn_auto_set,omitempty"` + MsgInputStickyComposer bool `json:"msg_input_sticky_composer,omitempty"` + GraphicEmoticons bool `json:"graphic_emoticons,omitempty"` + ConvertEmoticons bool `json:"convert_emoticons,omitempty"` + SsEmojis bool `json:"ss_emojis,omitempty"` + SeenOnboardingStart bool `json:"seen_onboarding_start,omitempty"` + OnboardingCancelled bool `json:"onboarding_cancelled,omitempty"` + SeenOnboardingSlackbotConversation bool `json:"seen_onboarding_slackbot_conversation,omitempty"` + SeenOnboardingChannels bool `json:"seen_onboarding_channels,omitempty"` + SeenOnboardingDirectMessages bool `json:"seen_onboarding_direct_messages,omitempty"` + SeenOnboardingInvites bool `json:"seen_onboarding_invites,omitempty"` + SeenOnboardingSearch bool `json:"seen_onboarding_search,omitempty"` + SeenOnboardingRecentMentions bool `json:"seen_onboarding_recent_mentions,omitempty"` + SeenOnboardingStarredItems bool `json:"seen_onboarding_starred_items,omitempty"` + SeenOnboardingPrivateGroups bool `json:"seen_onboarding_private_groups,omitempty"` + SeenOnboardingBanner bool `json:"seen_onboarding_banner,omitempty"` + OnboardingSlackbotConversationStep int `json:"onboarding_slackbot_conversation_step,omitempty"` + SetTzAutomatically bool `json:"set_tz_automatically,omitempty"` + SuppressLinkWarning bool `json:"suppress_link_warning,omitempty"` + DndEnabled bool `json:"dnd_enabled,omitempty"` + DndStartHour string `json:"dnd_start_hour,omitempty"` + DndEndHour string `json:"dnd_end_hour,omitempty"` + DndBeforeMonday string `json:"dnd_before_monday,omitempty"` + DndAfterMonday string `json:"dnd_after_monday,omitempty"` + DndEnabledMonday string `json:"dnd_enabled_monday,omitempty"` + DndBeforeTuesday string `json:"dnd_before_tuesday,omitempty"` + DndAfterTuesday string `json:"dnd_after_tuesday,omitempty"` + DndEnabledTuesday string `json:"dnd_enabled_tuesday,omitempty"` + DndBeforeWednesday string `json:"dnd_before_wednesday,omitempty"` + DndAfterWednesday string `json:"dnd_after_wednesday,omitempty"` + DndEnabledWednesday string `json:"dnd_enabled_wednesday,omitempty"` + DndBeforeThursday string `json:"dnd_before_thursday,omitempty"` + DndAfterThursday string `json:"dnd_after_thursday,omitempty"` + DndEnabledThursday string `json:"dnd_enabled_thursday,omitempty"` + DndBeforeFriday string `json:"dnd_before_friday,omitempty"` + DndAfterFriday string `json:"dnd_after_friday,omitempty"` + DndEnabledFriday string `json:"dnd_enabled_friday,omitempty"` + DndBeforeSaturday string `json:"dnd_before_saturday,omitempty"` + DndAfterSaturday string `json:"dnd_after_saturday,omitempty"` + DndEnabledSaturday string `json:"dnd_enabled_saturday,omitempty"` + DndBeforeSunday string `json:"dnd_before_sunday,omitempty"` + DndAfterSunday string `json:"dnd_after_sunday,omitempty"` + DndEnabledSunday string `json:"dnd_enabled_sunday,omitempty"` + DndDays string `json:"dnd_days,omitempty"` + DndCustomNewBadgeSeen bool `json:"dnd_custom_new_badge_seen,omitempty"` + DndNotificationScheduleNewBadgeSeen bool `json:"dnd_notification_schedule_new_badge_seen,omitempty"` + // UnreadCollapsedChannels unknownType `json:"unread_collapsed_channels,omitempty"` + SidebarBehavior string `json:"sidebar_behavior,omitempty"` + ChannelSort string `json:"channel_sort,omitempty"` + SeparatePrivateChannels bool `json:"separate_private_channels,omitempty"` + SeparateSharedChannels bool `json:"separate_shared_channels,omitempty"` + SidebarTheme string `json:"sidebar_theme,omitempty"` + SidebarThemeCustomValues string `json:"sidebar_theme_custom_values,omitempty"` + NoInvitesWidgetInSidebar bool `json:"no_invites_widget_in_sidebar,omitempty"` + NoOmniboxInChannels bool `json:"no_omnibox_in_channels,omitempty"` + + KKeyOmniboxAutoHideCount int `json:"k_key_omnibox_auto_hide_count,omitempty"` + ShowSidebarQuickswitcherButton bool `json:"show_sidebar_quickswitcher_button,omitempty"` + EntOrgWideChannelsSidebar bool `json:"ent_org_wide_channels_sidebar,omitempty"` + MarkMsgsReadImmediately bool `json:"mark_msgs_read_immediately,omitempty"` + StartScrollAtOldest bool `json:"start_scroll_at_oldest,omitempty"` + SnippetEditorWrapLongLines bool `json:"snippet_editor_wrap_long_lines,omitempty"` + LsDisabled bool `json:"ls_disabled,omitempty"` + FKeySearch bool `json:"f_key_search,omitempty"` + KKeyOmnibox bool `json:"k_key_omnibox,omitempty"` + PromptedForEmailDisabling bool `json:"prompted_for_email_disabling,omitempty"` + NoMacelectronBanner bool `json:"no_macelectron_banner,omitempty"` + NoMacssb1Banner bool `json:"no_macssb1_banner,omitempty"` + NoMacssb2Banner bool `json:"no_macssb2_banner,omitempty"` + NoWinssb1Banner bool `json:"no_winssb1_banner,omitempty"` + HideUserGroupInfoPane bool `json:"hide_user_group_info_pane,omitempty"` + MentionsExcludeAtUserGroups bool `json:"mentions_exclude_at_user_groups,omitempty"` + MentionsExcludeReactions bool `json:"mentions_exclude_reactions,omitempty"` + PrivacyPolicySeen bool `json:"privacy_policy_seen,omitempty"` + EnterpriseMigrationSeen bool `json:"enterprise_migration_seen,omitempty"` + LastTosAcknowledged string `json:"last_tos_acknowledged,omitempty"` + SearchExcludeBots bool `json:"search_exclude_bots,omitempty"` + LoadLato2 bool `json:"load_lato_2,omitempty"` + FullerTimestamps bool `json:"fuller_timestamps,omitempty"` + LastSeenAtChannelWarning int `json:"last_seen_at_channel_warning,omitempty"` + EmojiAutocompleteBig bool `json:"emoji_autocomplete_big,omitempty"` + TwoFactorAuthEnabled bool `json:"two_factor_auth_enabled,omitempty"` + // TwoFactorType unknownType `json:"two_factor_type,omitempty"` + // TwoFactorBackupType unknownType `json:"two_factor_backup_type,omitempty"` + HideHexSwatch bool `json:"hide_hex_swatch,omitempty"` + ShowJumperScores bool `json:"show_jumper_scores,omitempty"` + EnterpriseMdmCustomMsg string `json:"enterprise_mdm_custom_msg,omitempty"` + // EnterpriseExcludedAppTeams unknownType `json:"enterprise_excluded_app_teams,omitempty"` + ClientLogsPri string `json:"client_logs_pri,omitempty"` + FlannelServerPool string `json:"flannel_server_pool,omitempty"` + MentionsExcludeAtChannels bool `json:"mentions_exclude_at_channels,omitempty"` + ConfirmClearAllUnreads bool `json:"confirm_clear_all_unreads,omitempty"` + ConfirmUserMarkedAway bool `json:"confirm_user_marked_away,omitempty"` + BoxEnabled bool `json:"box_enabled,omitempty"` + SeenSingleEmojiMsg bool `json:"seen_single_emoji_msg,omitempty"` + ConfirmShCallStart bool `json:"confirm_sh_call_start,omitempty"` + PreferredSkinTone string `json:"preferred_skin_tone,omitempty"` + ShowAllSkinTones bool `json:"show_all_skin_tones,omitempty"` + WhatsNewRead int `json:"whats_new_read,omitempty"` + // FrecencyJumper unknownType `json:"frecency_jumper,omitempty"` + FrecencyEntJumper string `json:"frecency_ent_jumper,omitempty"` + FrecencyEntJumperBackup string `json:"frecency_ent_jumper_backup,omitempty"` + Jumbomoji bool `json:"jumbomoji,omitempty"` + NewxpSeenLastMessage int `json:"newxp_seen_last_message,omitempty"` + ShowMemoryInstrument bool `json:"show_memory_instrument,omitempty"` + EnableUnreadView bool `json:"enable_unread_view,omitempty"` + SeenUnreadViewCoachmark bool `json:"seen_unread_view_coachmark,omitempty"` + EnableReactEmojiPicker bool `json:"enable_react_emoji_picker,omitempty"` + SeenCustomStatusBadge bool `json:"seen_custom_status_badge,omitempty"` + SeenCustomStatusCallout bool `json:"seen_custom_status_callout,omitempty"` + SeenCustomStatusExpirationBadge bool `json:"seen_custom_status_expiration_badge,omitempty"` + UsedCustomStatusKbShortcut bool `json:"used_custom_status_kb_shortcut,omitempty"` + SeenGuestAdminSlackbotAnnouncement bool `json:"seen_guest_admin_slackbot_announcement,omitempty"` + SeenThreadsNotificationBanner bool `json:"seen_threads_notification_banner,omitempty"` + SeenNameTaggingCoachmark bool `json:"seen_name_tagging_coachmark,omitempty"` + AllUnreadsSortOrder string `json:"all_unreads_sort_order,omitempty"` + Locale string `json:"locale,omitempty"` + SeenIntlChannelNamesCoachmark bool `json:"seen_intl_channel_names_coachmark,omitempty"` + SeenP2LocaleChangeMessage int `json:"seen_p2_locale_change_message,omitempty"` + SeenLocaleChangeMessage int `json:"seen_locale_change_message,omitempty"` + SeenJapaneseLocaleChangeMessage bool `json:"seen_japanese_locale_change_message,omitempty"` + SeenSharedChannelsCoachmark bool `json:"seen_shared_channels_coachmark,omitempty"` + SeenSharedChannelsOptInChangeMessage bool `json:"seen_shared_channels_opt_in_change_message,omitempty"` + HasRecentlySharedaChannel bool `json:"has_recently_shared_a_channel,omitempty"` + SeenChannelBrowserAdminCoachmark bool `json:"seen_channel_browser_admin_coachmark,omitempty"` + SeenAdministrationMenu bool `json:"seen_administration_menu,omitempty"` + SeenDraftsSectionCoachmark bool `json:"seen_drafts_section_coachmark,omitempty"` + SeenEmojiUpdateOverlayCoachmark bool `json:"seen_emoji_update_overlay_coachmark,omitempty"` + SeenSonicDeluxeToast int `json:"seen_sonic_deluxe_toast,omitempty"` + SeenWysiwygDeluxeToast bool `json:"seen_wysiwyg_deluxe_toast,omitempty"` + SeenMarkdownPasteToast int `json:"seen_markdown_paste_toast,omitempty"` + SeenMarkdownPasteShortcut int `json:"seen_markdown_paste_shortcut,omitempty"` + SeenIaEducation bool `json:"seen_ia_education,omitempty"` + PlainTextMode bool `json:"plain_text_mode,omitempty"` + ShowSharedChannelsEducationBanner bool `json:"show_shared_channels_education_banner,omitempty"` + AllowCallsToSetCurrentStatus bool `json:"allow_calls_to_set_current_status,omitempty"` + InInteractiveMasMigrationFlow bool `json:"in_interactive_mas_migration_flow,omitempty"` + SunsetInteractiveMessageViews int `json:"sunset_interactive_message_views,omitempty"` + ShdepPromoCodeSubmitted bool `json:"shdep_promo_code_submitted,omitempty"` + SeenShdepSlackbotMessage bool `json:"seen_shdep_slackbot_message,omitempty"` + SeenCallsInteractiveCoachmark bool `json:"seen_calls_interactive_coachmark,omitempty"` + AllowCmdTabIss bool `json:"allow_cmd_tab_iss,omitempty"` + SeenWorkflowBuilderDeluxeToast bool `json:"seen_workflow_builder_deluxe_toast,omitempty"` + WorkflowBuilderIntroModalClickedThrough bool `json:"workflow_builder_intro_modal_clicked_through,omitempty"` + // WorkflowBuilderCoachmarks unknownType `json:"workflow_builder_coachmarks,omitempty"` + SeenGdriveCoachmark bool `json:"seen_gdrive_coachmark,omitempty"` + OverloadedMessageEnabled bool `json:"overloaded_message_enabled,omitempty"` + SeenHighlightsCoachmark bool `json:"seen_highlights_coachmark,omitempty"` + SeenHighlightsArrowsCoachmark bool `json:"seen_highlights_arrows_coachmark,omitempty"` + SeenHighlightsWarmWelcome bool `json:"seen_highlights_warm_welcome,omitempty"` + SeenNewSearchUi bool `json:"seen_new_search_ui,omitempty"` + SeenChannelSearch bool `json:"seen_channel_search,omitempty"` + SeenPeopleSearch bool `json:"seen_people_search,omitempty"` + SeenPeopleSearchCount int `json:"seen_people_search_count,omitempty"` + DismissedScrollSearchTooltipCount int `json:"dismissed_scroll_search_tooltip_count,omitempty"` + LastDismissedScrollSearchTooltipTimestamp int `json:"last_dismissed_scroll_search_tooltip_timestamp,omitempty"` + HasUsedQuickswitcherShortcut bool `json:"has_used_quickswitcher_shortcut,omitempty"` + SeenQuickswitcherShortcutTipCount int `json:"seen_quickswitcher_shortcut_tip_count,omitempty"` + BrowsersDismissedChannelsLowResultsEducation bool `json:"browsers_dismissed_channels_low_results_education,omitempty"` + BrowsersSeenInitialChannelsEducation bool `json:"browsers_seen_initial_channels_education,omitempty"` + BrowsersDismissedPeopleLowResultsEducation bool `json:"browsers_dismissed_people_low_results_education,omitempty"` + BrowsersSeenInitialPeopleEducation bool `json:"browsers_seen_initial_people_education,omitempty"` + BrowsersDismissedUserGroupsLowResultsEducation bool `json:"browsers_dismissed_user_groups_low_results_education,omitempty"` + BrowsersSeenInitialUserGroupsEducation bool `json:"browsers_seen_initial_user_groups_education,omitempty"` + BrowsersDismissedFilesLowResultsEducation bool `json:"browsers_dismissed_files_low_results_education,omitempty"` + BrowsersSeenInitialFilesEducation bool `json:"browsers_seen_initial_files_education,omitempty"` + A11yAnimations bool `json:"a11y_animations,omitempty"` + SeenKeyboardShortcutsCoachmark bool `json:"seen_keyboard_shortcuts_coachmark,omitempty"` + NeedsInitialPasswordSet bool `json:"needs_initial_password_set,omitempty"` + LessonsEnabled bool `json:"lessons_enabled,omitempty"` + TractorEnabled bool `json:"tractor_enabled,omitempty"` + TractorExperimentGroup string `json:"tractor_experiment_group,omitempty"` + OpenedSlackbotDm bool `json:"opened_slackbot_dm,omitempty"` + NewxpSuggestedChannels string `json:"newxp_suggested_channels,omitempty"` + OnboardingComplete bool `json:"onboarding_complete,omitempty"` + WelcomePlaceState string `json:"welcome_place_state,omitempty"` + // OnboardingRoleApps unknownType `json:"onboarding_role_apps,omitempty"` + HasReceivedThreadedMessage bool `json:"has_received_threaded_message,omitempty"` + SendYourFirstMessageBannerEnabled bool `json:"send_your_first_message_banner_enabled,omitempty"` + WhocanseethisDmMpdmBadge bool `json:"whocanseethis_dm_mpdm_badge,omitempty"` + HighlightWords string `json:"highlight_words,omitempty"` + ThreadsEverything bool `json:"threads_everything,omitempty"` + NoTextInNotifications bool `json:"no_text_in_notifications,omitempty"` + PushShowPreview bool `json:"push_show_preview,omitempty"` + GrowlsEnabled bool `json:"growls_enabled,omitempty"` + AllChannelsLoud bool `json:"all_channels_loud,omitempty"` + PushDmAlert bool `json:"push_dm_alert,omitempty"` + PushMentionAlert bool `json:"push_mention_alert,omitempty"` + PushEverything bool `json:"push_everything,omitempty"` + PushIdleWait int `json:"push_idle_wait,omitempty"` + PushSound string `json:"push_sound,omitempty"` + NewMsgSnd string `json:"new_msg_snd,omitempty"` + PushLoudChannels string `json:"push_loud_channels,omitempty"` + PushMentionChannels string `json:"push_mention_channels,omitempty"` + PushLoudChannelsSet string `json:"push_loud_channels_set,omitempty"` + LoudChannels string `json:"loud_channels,omitempty"` + NeverChannels string `json:"never_channels,omitempty"` + LoudChannelsSet string `json:"loud_channels_set,omitempty"` + AtChannelSuppressedChannels string `json:"at_channel_suppressed_channels,omitempty"` + PushAtChannelSuppressedChannels string `json:"push_at_channel_suppressed_channels,omitempty"` + MutedChannels string `json:"muted_channels,omitempty"` + // AllNotificationsPrefs unknownType `json:"all_notifications_prefs,omitempty"` + GrowthMsgLimitApproachingCtaCount int `json:"growth_msg_limit_approaching_cta_count,omitempty"` + GrowthMsgLimitApproachingCtaTs int `json:"growth_msg_limit_approaching_cta_ts,omitempty"` + GrowthMsgLimitReachedCtaCount int `json:"growth_msg_limit_reached_cta_count,omitempty"` + GrowthMsgLimitReachedCtaLastTs int `json:"growth_msg_limit_reached_cta_last_ts,omitempty"` + GrowthMsgLimitLongReachedCtaCount int `json:"growth_msg_limit_long_reached_cta_count,omitempty"` + GrowthMsgLimitLongReachedCtaLastTs int `json:"growth_msg_limit_long_reached_cta_last_ts,omitempty"` + GrowthMsgLimitSixtyDayBannerCtaCount int `json:"growth_msg_limit_sixty_day_banner_cta_count,omitempty"` + GrowthMsgLimitSixtyDayBannerCtaLastTs int `json:"growth_msg_limit_sixty_day_banner_cta_last_ts,omitempty"` + // GrowthAllBannersPrefs unknownType `json:"growth_all_banners_prefs,omitempty"` + AnalyticsUpsellCoachmarkSeen bool `json:"analytics_upsell_coachmark_seen,omitempty"` + SeenAppSpaceCoachmark bool `json:"seen_app_space_coachmark,omitempty"` + SeenAppSpaceTutorial bool `json:"seen_app_space_tutorial,omitempty"` + DismissedAppLauncherWelcome bool `json:"dismissed_app_launcher_welcome,omitempty"` + DismissedAppLauncherLimit bool `json:"dismissed_app_launcher_limit,omitempty"` + Purchaser bool `json:"purchaser,omitempty"` + ShowEntOnboarding bool `json:"show_ent_onboarding,omitempty"` + FoldersEnabled bool `json:"folders_enabled,omitempty"` + // FolderData unknownType `json:"folder_data,omitempty"` + SeenCorporateExportAlert bool `json:"seen_corporate_export_alert,omitempty"` + ShowAutocompleteHelp int `json:"show_autocomplete_help,omitempty"` + DeprecationToastLastSeen int `json:"deprecation_toast_last_seen,omitempty"` + DeprecationModalLastSeen int `json:"deprecation_modal_last_seen,omitempty"` + Iap1Lab int `json:"iap1_lab,omitempty"` + IaTopNavTheme string `json:"ia_top_nav_theme,omitempty"` + IaPlatformActionsLab int `json:"ia_platform_actions_lab,omitempty"` + ActivityView string `json:"activity_view,omitempty"` + FailoverProxyCheckCompleted int `json:"failover_proxy_check_completed,omitempty"` + EdgeUploadProxyCheckCompleted int `json:"edge_upload_proxy_check_completed,omitempty"` + AppSubdomainCheckCompleted int `json:"app_subdomain_check_completed,omitempty"` + AddAppsPromptDismissed bool `json:"add_apps_prompt_dismissed,omitempty"` + AddChannelPromptDismissed bool `json:"add_channel_prompt_dismissed,omitempty"` + ChannelSidebarHideInvite bool `json:"channel_sidebar_hide_invite,omitempty"` + InProdSurveysEnabled bool `json:"in_prod_surveys_enabled,omitempty"` + DismissedInstalledAppDmSuggestions string `json:"dismissed_installed_app_dm_suggestions,omitempty"` + SeenContextualMessageShortcutsModal bool `json:"seen_contextual_message_shortcuts_modal,omitempty"` + SeenMessageNavigationEducationalToast bool `json:"seen_message_navigation_educational_toast,omitempty"` + ContextualMessageShortcutsModalWasSeen bool `json:"contextual_message_shortcuts_modal_was_seen,omitempty"` + MessageNavigationToastWasSeen bool `json:"message_navigation_toast_was_seen,omitempty"` + UpToBrowseKbShortcut bool `json:"up_to_browse_kb_shortcut,omitempty"` + ChannelSections string `json:"channel_sections,omitempty"` + TZ string `json:"tz,omitempty"` +} + +func (api *Client) GetUserPrefs() (*UserPrefsCarrier, error) { + values := url.Values{"token": {api.token}} + response := UserPrefsCarrier{} + + err := api.getMethod(context.Background(), "users.prefs.get", values, &response) + if err != nil { + return nil, err + } + + return &response, response.Err() +} + +func (api *Client) MuteChat(channelID string) (*UserPrefsCarrier, error) { + prefs, err := api.GetUserPrefs() + if err != nil { + return nil, err + } + chnls := strings.Split(prefs.UserPrefs.MutedChannels, ",") + for _, chn := range chnls { + if chn == channelID { + return nil, nil // noop + } + } + newChnls := prefs.UserPrefs.MutedChannels + "," + channelID + values := url.Values{"token": {api.token}, "muted_channels": {newChnls}, "reason": {"update-muted-channels"}} + response := UserPrefsCarrier{} + + err = api.postMethod(context.Background(), "users.prefs.set", values, &response) + if err != nil { + return nil, err + } + + return &response, response.Err() +} + +func (api *Client) UnMuteChat(channelID string) (*UserPrefsCarrier, error) { + prefs, err := api.GetUserPrefs() + if err != nil { + return nil, err + } + chnls := strings.Split(prefs.UserPrefs.MutedChannels, ",") + newChnls := make([]string, len(chnls)-1) + for i, chn := range chnls { + if chn == channelID { + return nil, nil // noop + } + newChnls[i] = chn + } + values := url.Values{"token": {api.token}, "muted_channels": {strings.Join(newChnls, ",")}, "reason": {"update-muted-channels"}} + response := UserPrefsCarrier{} + + err = api.postMethod(context.Background(), "users.prefs.set", values, &response) + if err != nil { + return nil, err + } + + return &response, response.Err() +} + +// UserDetails contains user details coming in the initial response from StartRTM +type UserDetails struct { + ID string `json:"id"` + Name string `json:"name"` + Created JSONTime `json:"created"` + ManualPresence string `json:"manual_presence"` + Prefs UserPrefs `json:"prefs"` +} + +// JSONTime exists so that we can have a String method converting the date +type JSONTime int64 + +// String converts the unix timestamp into a string +func (t JSONTime) String() string { + tm := t.Time() + return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) +} + +// Time returns a `time.Time` representation of this value. +func (t JSONTime) Time() time.Time { + return time.Unix(int64(t), 0) +} + +// UnmarshalJSON will unmarshal both string and int JSON values +func (t *JSONTime) UnmarshalJSON(buf []byte) error { + s := bytes.Trim(buf, `"`) + + v, err := strconv.Atoi(string(s)) + if err != nil { + return err + } + + *t = JSONTime(int64(v)) + return nil +} + +// Team contains details about a team +type Team struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` +} + +// Icons XXX: needs further investigation +type Icons struct { + Image36 string `json:"image_36,omitempty"` + Image48 string `json:"image_48,omitempty"` + Image72 string `json:"image_72,omitempty"` +} + +// Info contains various details about the authenticated user and team. +// It is returned by StartRTM or included in the "ConnectedEvent" RTM event. +type Info struct { + URL string `json:"url,omitempty"` + User *UserDetails `json:"self,omitempty"` + Team *Team `json:"team,omitempty"` +} + +type infoResponseFull struct { + Info + SlackResponse +} + +// GetBotByID is deprecated and returns nil +func (info Info) GetBotByID(botID string) *Bot { + return nil +} + +// GetUserByID is deprecated and returns nil +func (info Info) GetUserByID(userID string) *User { + return nil +} + +// GetChannelByID is deprecated and returns nil +func (info Info) GetChannelByID(channelID string) *Channel { + return nil +} + +// GetGroupByID is deprecated and returns nil +func (info Info) GetGroupByID(groupID string) *Group { + return nil +} + +// GetIMByID is deprecated and returns nil +func (info Info) GetIMByID(imID string) *IM { + return nil +} diff --git a/backend/vendor/github.com/slack-go/slack/interactions.go b/backend/vendor/github.com/slack-go/slack/interactions.go new file mode 100644 index 00000000..2515e290 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/interactions.go @@ -0,0 +1,221 @@ +package slack + +import ( + "bytes" + "encoding/json" +) + +// InteractionType type of interactions +type InteractionType string + +// ActionType type represents the type of action (attachment, block, etc.) +type actionType string + +// action is an interface that should be implemented by all callback action types +type action interface { + actionType() actionType +} + +// Types of interactions that can be received. +const ( + InteractionTypeDialogCancellation = InteractionType("dialog_cancellation") + InteractionTypeDialogSubmission = InteractionType("dialog_submission") + InteractionTypeDialogSuggestion = InteractionType("dialog_suggestion") + InteractionTypeInteractionMessage = InteractionType("interactive_message") + InteractionTypeMessageAction = InteractionType("message_action") + InteractionTypeBlockActions = InteractionType("block_actions") + InteractionTypeBlockSuggestion = InteractionType("block_suggestion") + InteractionTypeViewSubmission = InteractionType("view_submission") + InteractionTypeViewClosed = InteractionType("view_closed") + InteractionTypeShortcut = InteractionType("shortcut") +) + +// InteractionCallback is sent from slack when a user interactions with a button or dialog. +type InteractionCallback struct { + Type InteractionType `json:"type"` + Token string `json:"token"` + CallbackID string `json:"callback_id"` + ResponseURL string `json:"response_url"` + TriggerID string `json:"trigger_id"` + ActionTs string `json:"action_ts"` + Team Team `json:"team"` + Channel Channel `json:"channel"` + User User `json:"user"` + OriginalMessage Message `json:"original_message"` + Message Message `json:"message"` + Name string `json:"name"` + Value string `json:"value"` + MessageTs string `json:"message_ts"` + AttachmentID string `json:"attachment_id"` + ActionCallback ActionCallbacks `json:"actions"` + View View `json:"view"` + ActionID string `json:"action_id"` + APIAppID string `json:"api_app_id"` + BlockID string `json:"block_id"` + Container Container `json:"container"` + DialogSubmissionCallback + ViewSubmissionCallback + ViewClosedCallback + + // FIXME(kanata2): just workaround for backward-compatibility. + // See also https://github.com/slack-go/slack/issues/816 + RawState json.RawMessage `json:"state,omitempty"` + + // BlockActionState stands for the `state` field in block_actions type. + // NOTE: InteractionCallback.State has a role for the state of dialog_submission type, + // so we cannot use this field for backward-compatibility for now. + BlockActionState *BlockActionStates `json:"-"` +} + +type BlockActionStates struct { + Values map[string]map[string]BlockAction `json:"values"` +} + +func (ic *InteractionCallback) MarshalJSON() ([]byte, error) { + type alias InteractionCallback + tmp := alias(*ic) + if tmp.Type == InteractionTypeBlockActions { + if tmp.BlockActionState == nil { + tmp.RawState = []byte(`{}`) + } else { + state, err := json.Marshal(tmp.BlockActionState.Values) + if err != nil { + return nil, err + } + tmp.RawState = []byte(`{"values":` + string(state) + `}`) + } + } else if ic.Type == InteractionTypeDialogSubmission { + tmp.RawState = []byte(tmp.State) + } + // Use pointer for go1.7 + return json.Marshal(&tmp) +} + +func (ic *InteractionCallback) UnmarshalJSON(b []byte) error { + type alias InteractionCallback + tmp := struct { + Type InteractionType `json:"type"` + *alias + }{ + alias: (*alias)(ic), + } + if err := json.Unmarshal(b, &tmp); err != nil { + return err + } + *ic = InteractionCallback(*tmp.alias) + ic.Type = tmp.Type + if ic.Type == InteractionTypeBlockActions { + if len(ic.RawState) > 0 { + err := json.Unmarshal(ic.RawState, &ic.BlockActionState) + if err != nil { + return err + } + } + } else if ic.Type == InteractionTypeDialogSubmission { + ic.State = string(ic.RawState) + } + return nil +} + +type Container struct { + Type string `json:"type"` + ViewID string `json:"view_id"` + MessageTs string `json:"message_ts"` + AttachmentID json.Number `json:"attachment_id"` + ChannelID string `json:"channel_id"` + IsEphemeral bool `json:"is_ephemeral"` + IsAppUnfurl bool `json:"is_app_unfurl"` +} + +// ActionCallback is a convenience struct defined to allow dynamic unmarshalling of +// the "actions" value in Slack's JSON response, which varies depending on block type +type ActionCallbacks struct { + AttachmentActions []*AttachmentAction + BlockActions []*BlockAction +} + +// MarshalJSON implements the Marshaller interface in order to combine both +// action callback types back into a single array, like how the api responds. +// This makes Marshaling and Unmarshaling an InteractionCallback symmetrical +func (a ActionCallbacks) MarshalJSON() ([]byte, error) { + count := 0 + length := len(a.AttachmentActions) + len(a.BlockActions) + buffer := bytes.NewBufferString("[") + + f := func(obj interface{}) error { + js, err := json.Marshal(obj) + if err != nil { + return err + } + _, err = buffer.Write(js) + if err != nil { + return err + } + + count++ + if count < length { + _, err = buffer.WriteString(",") + return err + } + return nil + } + + for _, act := range a.AttachmentActions { + err := f(act) + if err != nil { + return nil, err + } + } + for _, blk := range a.BlockActions { + err := f(blk) + if err != nil { + return nil, err + } + } + buffer.WriteString("]") + return buffer.Bytes(), nil +} + +// UnmarshalJSON implements the Marshaller interface in order to delegate +// marshalling and allow for proper type assertion when decoding the response +func (a *ActionCallbacks) UnmarshalJSON(data []byte) error { + var raw []json.RawMessage + err := json.Unmarshal(data, &raw) + if err != nil { + return err + } + + for _, r := range raw { + var obj map[string]interface{} + err := json.Unmarshal(r, &obj) + if err != nil { + return err + } + + if _, ok := obj["block_id"].(string); ok { + action, err := unmarshalAction(r, &BlockAction{}) + if err != nil { + return err + } + + a.BlockActions = append(a.BlockActions, action.(*BlockAction)) + continue + } + + action, err := unmarshalAction(r, &AttachmentAction{}) + if err != nil { + return err + } + a.AttachmentActions = append(a.AttachmentActions, action.(*AttachmentAction)) + } + + return nil +} + +func unmarshalAction(r json.RawMessage, callbackAction action) (action, error) { + err := json.Unmarshal(r, callbackAction) + if err != nil { + return nil, err + } + return callbackAction, nil +} diff --git a/backend/vendor/github.com/slack-go/slack/internal/backoff/backoff.go b/backend/vendor/github.com/slack-go/slack/internal/backoff/backoff.go new file mode 100644 index 00000000..df210f80 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/internal/backoff/backoff.go @@ -0,0 +1,62 @@ +package backoff + +import ( + "math/rand" + "time" +) + +// This one was ripped from https://github.com/jpillora/backoff/blob/master/backoff.go + +// Backoff is a time.Duration counter. It starts at Min. After every +// 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 { + attempts int + // Initial value to scale out + Initial time.Duration + // Jitter value randomizes an additional delay between 0 and Jitter + Jitter time.Duration + // Max maximum values of the backoff + Max time.Duration +} + +// Returns the current value of the counter and then multiplies it +// Factor +func (b *Backoff) Duration() (dur time.Duration) { + // Zero-values are nonsensical, so we use + // them to apply defaults + if b.Max == 0 { + b.Max = 10 * time.Second + } + + if b.Initial == 0 { + b.Initial = 100 * time.Millisecond + } + + // calculate this duration + if dur = time.Duration(1 << uint(b.attempts)); dur > 0 { + dur = dur * b.Initial + } else { + dur = b.Max + } + + if b.Jitter > 0 { + dur = dur + time.Duration(rand.Intn(int(b.Jitter))) + } + + // bump attempts count + b.attempts++ + + return dur +} + +//Resets the current value of the counter back to Min +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 +} diff --git a/backend/vendor/github.com/slack-go/slack/internal/errorsx/errorsx.go b/backend/vendor/github.com/slack-go/slack/internal/errorsx/errorsx.go new file mode 100644 index 00000000..0182ec68 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/internal/errorsx/errorsx.go @@ -0,0 +1,17 @@ +package errorsx + +// String representing an error, useful for declaring string constants as errors. +type String string + +func (t String) Error() string { + return string(t) +} + +// Is reports whether String matches with the target error +func (t String) Is(target error) bool { + if target == nil { + return false + } + + return t.Error() == target.Error() +} diff --git a/backend/vendor/github.com/slack-go/slack/internal/misc/misc.go b/backend/vendor/github.com/slack-go/slack/internal/misc/misc.go new file mode 100644 index 00000000..eab8cdd8 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/internal/misc/misc.go @@ -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 +} diff --git a/backend/vendor/github.com/slack-go/slack/internal/timex/timex.go b/backend/vendor/github.com/slack-go/slack/internal/timex/timex.go new file mode 100644 index 00000000..40063f73 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/internal/timex/timex.go @@ -0,0 +1,18 @@ +package timex + +import "time" + +// Max returns the maximum duration +func Max(values ...time.Duration) time.Duration { + var ( + max time.Duration + ) + + for _, v := range values { + if v > max { + max = v + } + } + + return max +} diff --git a/backend/vendor/github.com/slack-go/slack/item.go b/backend/vendor/github.com/slack-go/slack/item.go new file mode 100644 index 00000000..89af4eb1 --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/item.go @@ -0,0 +1,75 @@ +package slack + +const ( + TYPE_MESSAGE = "message" + TYPE_FILE = "file" + TYPE_FILE_COMMENT = "file_comment" + TYPE_CHANNEL = "channel" + TYPE_IM = "im" + TYPE_GROUP = "group" +) + +// Item is any type of slack message - message, file, or file comment. +type Item struct { + Type string `json:"type"` + Channel string `json:"channel,omitempty"` + Message *Message `json:"message,omitempty"` + File *File `json:"file,omitempty"` + Comment *Comment `json:"comment,omitempty"` + Timestamp string `json:"ts,omitempty"` +} + +// NewMessageItem turns a message on a channel into a typed message struct. +func NewMessageItem(ch string, m *Message) Item { + return Item{Type: TYPE_MESSAGE, Channel: ch, Message: m} +} + +// NewFileItem turns a file into a typed file struct. +func NewFileItem(f *File) Item { + return Item{Type: TYPE_FILE, File: f} +} + +// NewFileCommentItem turns a file and comment into a typed file_comment struct. +func NewFileCommentItem(f *File, c *Comment) Item { + return Item{Type: TYPE_FILE_COMMENT, File: f, Comment: c} +} + +// NewChannelItem turns a channel id into a typed channel struct. +func NewChannelItem(ch string) Item { + return Item{Type: TYPE_CHANNEL, Channel: ch} +} + +// NewIMItem turns a channel id into a typed im struct. +func NewIMItem(ch string) Item { + return Item{Type: TYPE_IM, Channel: ch} +} + +// NewGroupItem turns a channel id into a typed group struct. +func NewGroupItem(ch string) Item { + return Item{Type: TYPE_GROUP, Channel: ch} +} + +// ItemRef is a reference to a message of any type. One of FileID, +// CommentId, or the combination of ChannelId and Timestamp must be +// specified. +type ItemRef struct { + Channel string `json:"channel"` + Timestamp string `json:"timestamp"` + File string `json:"file"` + Comment string `json:"file_comment"` +} + +// NewRefToMessage initializes a reference to to a message. +func NewRefToMessage(channel, timestamp string) ItemRef { + return ItemRef{Channel: channel, Timestamp: timestamp} +} + +// NewRefToFile initializes a reference to a file. +func NewRefToFile(file string) ItemRef { + return ItemRef{File: file} +} + +// NewRefToComment initializes a reference to a file comment. +func NewRefToComment(comment string) ItemRef { + return ItemRef{Comment: comment} +} diff --git a/backend/vendor/github.com/slack-go/slack/logger.go b/backend/vendor/github.com/slack-go/slack/logger.go new file mode 100644 index 00000000..90cb3caa --- /dev/null +++ b/backend/vendor/github.com/slack-go/slack/logger.go @@ -0,0 +1,60 @@ +package slack + +import ( + "fmt" +) + +// logger is a logger interface compatible with both stdlib and some +// 3rd party loggers. +type logger interface { + Output(int, string) error +} + +// ilogger represents the internal logging api we use. +type ilogger interface { + logger + Print(...interface{}) + Printf(string, ...interface{}) + Println(...interface{}) +} + +type Debug interface { + Debug() bool + + // Debugf print a formatted debug line. + Debugf(format string, v ...interface{}) + // Debugln print a debug line. + Debugln(v ...interface{}) +} + +// internalLog implements the additional methods used by our internal logging. +type internalLog struct { + logger +} + +// Println replicates the behaviour of the standard logger. +func (t internalLog) Println(v ...interface{}) { + t.Output(2, fmt.Sprintln(v...)) +} + +// Printf replicates the behaviour of the standard logger. +func (t internalLog) Printf(format string, v ...interface{}) { + t.Output(2, fmt.Sprintf(format, v...)) +} + +// Print replicates the behaviour of the standard logger. +func (t internalLog) Print(v ...interface{}) { + t.Output(2, fmt.Sprint(v...)) +} + +type discard struct{} + +func (t discard) Debug() bool { + return false +} + +// Debugf print a formatted debug line. +func (t discard) Debugf(format string, v ...interface{}) {} + +// Debugln print a debug line. +func (t discard) Debugln(v ...interface{}) {} diff --git a/backend/vendor/github.com/slack-go/slack/logo.png b/backend/vendor/github.com/slack-go/slack/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9bd143459bfe55bb6e01e721623c51a367be7190 GIT binary patch literal 52440 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

G+=^K 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