diff --git a/go.mod b/go.mod index 58c65c96..1cbf1729 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/honeycombio/beeline-go v1.11.1 github.com/jinzhu/gorm v0.0.0-20160404144928-5174cc5c242a github.com/nicklaw5/helix v1.25.0 - github.com/nlopes/slack v0.0.0-20180905213137-8cf10c586222 + github.com/nlopes/slack v0.6.0 github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c github.com/robfig/cron v1.2.0 @@ -56,7 +56,6 @@ require ( github.com/kennygrant/sanitize v1.2.4 // indirect github.com/klauspost/compress v1.15.9 // indirect github.com/lib/pq v1.10.7 // indirect - github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect github.com/temoto/robotstxt v1.1.1 // indirect diff --git a/go.sum b/go.sum index 9d50739e..e8c64b7e 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,7 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/securecookie v0.0.0-20160422134519-667fe4e3466a h1:YH0IojQwndMQdeRWdw1aPT8bkbiWaYR3WD+Zf5e09DU= github.com/gorilla/securecookie v0.0.0-20160422134519-667fe4e3466a/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 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/honeycombio/beeline-go v1.11.1 h1:cyrfwgxM32DKzUhZFJ0KLbPkoyf5lHOyn+7GISwEVZQ= @@ -125,13 +126,11 @@ github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQan github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 h1:MNApn+Z+fIT4NPZopPfCc1obT6aY3SVM6DOctz1A9ZU= -github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= github.com/nicklaw5/helix v1.25.0 h1:Mrz537izZVsGdM3I46uGAAlslj61frgkhS/9xQqyT/M= github.com/nicklaw5/helix v1.25.0/go.mod h1:yvXZFapT6afIoxnAvlWiJiUMsYnoHl7tNs+t0bloAMw= -github.com/nlopes/slack v0.0.0-20180905213137-8cf10c586222 h1:JkYXciz3sQY/EVnZ1g9rS9HoKePa9R8rUgJVX9WdSUk= -github.com/nlopes/slack v0.0.0-20180905213137-8cf10c586222/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= +github.com/nlopes/slack v0.6.0 h1:jt0jxVQGhssx1Ib7naAOZEZcGdtIhTzkP0nopK0AsRA= +github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c h1:MUyE44mTvnI5A0xrxIxaMqoWFzPfQvtE2IWUollMDMs= diff --git a/vendor/github.com/nlopes/slack/.gometalinter.json b/vendor/github.com/nlopes/slack/.gometalinter.json new file mode 100644 index 00000000..5fa629d4 --- /dev/null +++ b/vendor/github.com/nlopes/slack/.gometalinter.json @@ -0,0 +1,14 @@ +{ + "DisableAll": true, + "Enable": [ + "structcheck", + "vet", + "misspell", + "unconvert", + "interfacer", + "goimports" + ], + "Vendor": true, + "Exclude": ["vendor"], + "Deadline": "300s" +} diff --git a/vendor/github.com/nlopes/slack/.travis.yml b/vendor/github.com/nlopes/slack/.travis.yml index bd0539e0..ed99d9ef 100644 --- a/vendor/github.com/nlopes/slack/.travis.yml +++ b/vendor/github.com/nlopes/slack/.travis.yml @@ -1,21 +1,35 @@ language: go -go: - - 1.7.x - - 1.8.x - - 1.9.x - - tip +env: + - GO111MODULE=on + +install: true before_install: - export PATH=$HOME/gopath/bin:$PATH + # install gometalinter + - curl -L https://git.io/vp6lP | sh script: - - go test -race ./... - - go test -cover ./... + - PATH=$PWD/bin:$PATH gometalinter ./... + - go test -race -cover ./... matrix: - allow_failures: - - go: tip + allow_failures: + - go: tip + include: + - go: "1.7.x" + script: go test -v ./... + - go: "1.8.x" + script: go test -v ./... + - go: "1.9.x" + script: go test -v ./... + - go: "1.10.x" + script: go test -v ./... + - go: "1.11.x" + script: go test -v -mod=vendor ./... + - go: "tip" + script: go test -v -mod=vendor ./... git: depth: 10 diff --git a/vendor/github.com/nlopes/slack/CHANGELOG.md b/vendor/github.com/nlopes/slack/CHANGELOG.md index a79ea50c..48bcce55 100644 --- a/vendor/github.com/nlopes/slack/CHANGELOG.md +++ b/vendor/github.com/nlopes/slack/CHANGELOG.md @@ -1,3 +1,37 @@ +### 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) diff --git a/vendor/github.com/nlopes/slack/Gopkg.lock b/vendor/github.com/nlopes/slack/Gopkg.lock deleted file mode 100644 index ed3d31e6..00000000 --- a/vendor/github.com/nlopes/slack/Gopkg.lock +++ /dev/null @@ -1,39 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - name = "github.com/davecgh/go-spew" - packages = ["spew"] - revision = "346938d642f2ec3594ed81d874461961cd0faa76" - version = "v1.1.0" - -[[projects]] - name = "github.com/gorilla/websocket" - packages = ["."] - revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" - version = "v1.2.0" - -[[projects]] - branch = "master" - name = "github.com/pkg/errors" - packages = ["."] - revision = "816c9085562cd7ee03e7f8188a1cfd942858cded" - -[[projects]] - name = "github.com/pmezard/go-difflib" - packages = ["difflib"] - revision = "792786c7400a136282c1664665ae0a8db921c6c2" - version = "v1.0.0" - -[[projects]] - name = "github.com/stretchr/testify" - packages = ["assert"] - revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" - version = "v1.2.2" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - inputs-digest = "596fa546322c2a1e9708a10c9f39aca2e04792b477fab86fb2899fbaab776070" - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/vendor/github.com/nlopes/slack/Gopkg.toml b/vendor/github.com/nlopes/slack/Gopkg.toml deleted file mode 100644 index 0d90da5d..00000000 --- a/vendor/github.com/nlopes/slack/Gopkg.toml +++ /dev/null @@ -1,17 +0,0 @@ -ignored = ["github.com/lusis/slack-test"] - -[[constraint]] - name = "github.com/gorilla/websocket" - version = "1.2.0" - -[[constraint]] - name = "github.com/stretchr/testify" - version = "1.2.1" - -[[constraint]] - name = "github.com/pkg/errors" - branch = "master" - -[prune] - go-tests = true - unused-packages = true diff --git a/vendor/github.com/nlopes/slack/README.md b/vendor/github.com/nlopes/slack/README.md index 849e8bdc..a5e8e5ef 100644 --- a/vendor/github.com/nlopes/slack/README.md +++ b/vendor/github.com/nlopes/slack/README.md @@ -9,18 +9,10 @@ a fully managed way. -## Change log -Support for the EventsAPI has recently been added. It is still in its early stages but nearly all events have been added and tested (except for those events in [Developer Preview](https://api.slack.com/slack-apps-preview) mode). API stability for events is not promised at this time. -### v0.2.0 - Feb 10, 2018 +## Changelog -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) - -### CHANGELOG.md - - [CHANGELOG.md](https://github.com/nlopes/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates. +[CHANGELOG.md](https://github.com/nlopes/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates. ## Installing @@ -43,7 +35,7 @@ func main() { api := slack.New("YOUR_TOKEN_HERE") // If you set debugging, it will log all requests to the console // Useful when encountering issues - // api.SetDebug(true) + // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true)) groups, err := api.GetGroups(false) if err != nil { fmt.Printf("%s\n", err) diff --git a/vendor/github.com/nlopes/slack/admin.go b/vendor/github.com/nlopes/slack/admin.go index a2aa7e5c..d51426b5 100644 --- a/vendor/github.com/nlopes/slack/admin.go +++ b/vendor/github.com/nlopes/slack/admin.go @@ -2,28 +2,19 @@ package slack import ( "context" - "errors" "fmt" "net/url" + "strings" ) -type adminResponse struct { - OK bool `json:"ok"` - Error string `json:"error"` -} - -func adminRequest(ctx context.Context, client HTTPRequester, method string, teamName string, values url.Values, debug bool) (*adminResponse, error) { - adminResponse := &adminResponse{} - err := parseAdminResponse(ctx, client, method, teamName, values, adminResponse, debug) +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 nil, err + return err } - if !adminResponse.OK { - return nil, errors.New(adminResponse.Error) - } - - return adminResponse, nil + return resp.Err() } // DisableUser disabled a user account, given a user ID @@ -40,9 +31,8 @@ func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "setInactive", teamName, values, api.debug) - if err != nil { - return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err) + 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 @@ -67,7 +57,7 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) + err := api.adminRequest(ctx, "invite", teamName, values) if err != nil { return fmt.Errorf("Failed to invite single-channel guest: %s", err) } @@ -94,7 +84,7 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) + err := api.adminRequest(ctx, "invite", teamName, values) if err != nil { return fmt.Errorf("Failed to restricted account: %s", err) } @@ -118,7 +108,7 @@ func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName, "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) + err := api.adminRequest(ctx, "invite", teamName, values) if err != nil { return fmt.Errorf("Failed to invite to team: %s", err) } @@ -140,7 +130,7 @@ func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "setRegular", teamName, values, api.debug) + 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) } @@ -162,7 +152,7 @@ func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, use "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "sendSSOBind", teamName, values, api.debug) + 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) } @@ -185,7 +175,7 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid, "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "setUltraRestricted", teamName, values, api.debug) + err := api.adminRequest(ctx, "setUltraRestricted", teamName, values) if err != nil { return fmt.Errorf("Failed to ultra-restrict account: %s", err) } @@ -194,22 +184,23 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid, } // SetRestricted converts a user into a restricted account -func (api *Client) SetRestricted(teamName, uid string) error { - return api.SetRestrictedContext(context.Background(), teamName, uid) +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) error { +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 := adminRequest(ctx, api.httpclient, "setRestricted", teamName, values, api.debug) + err := api.adminRequest(ctx, "setRestricted", teamName, values) if err != nil { - return fmt.Errorf("Failed to restrict account: %s", err) + return fmt.Errorf("failed to restrict account: %s", err) } return nil diff --git a/vendor/github.com/nlopes/slack/attachments.go b/vendor/github.com/nlopes/slack/attachments.go index 326fc010..cf8b5c67 100644 --- a/vendor/github.com/nlopes/slack/attachments.go +++ b/vendor/github.com/nlopes/slack/attachments.go @@ -17,7 +17,7 @@ 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 string `json:"type"` // Required. Must be set to "button" or "select". + 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. @@ -28,6 +28,11 @@ type AttachmentAction struct { 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. @@ -42,25 +47,8 @@ type AttachmentActionOptionGroup struct { } // AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction) -type AttachmentActionCallback struct { - Actions []AttachmentAction `json:"actions"` - CallbackID string `json:"callback_id"` - Team Team `json:"team"` - Channel Channel `json:"channel"` - User User `json:"user"` - - Name string `json:"name"` - Value string `json:"value"` - - OriginalMessage Message `json:"original_message"` - - ActionTs string `json:"action_ts"` - MessageTs string `json:"message_ts"` - AttachmentID string `json:"attachment_id"` - Token string `json:"token"` - ResponseURL string `json:"response_url"` - TriggerID string `json:"trigger_id"` -} +// DEPRECATED: use InteractionCallback +type AttachmentActionCallback InteractionCallback // ConfirmationField are used to ask users to confirm actions type ConfirmationField struct { diff --git a/vendor/github.com/nlopes/slack/auth.go b/vendor/github.com/nlopes/slack/auth.go new file mode 100644 index 00000000..dc1dbcdf --- /dev/null +++ b/vendor/github.com/nlopes/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 retrieve the satus from api.test +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/vendor/github.com/nlopes/slack/backoff.go b/vendor/github.com/nlopes/slack/backoff.go index 197bce2e..2ba697e7 100644 --- a/vendor/github.com/nlopes/slack/backoff.go +++ b/vendor/github.com/nlopes/slack/backoff.go @@ -1,7 +1,6 @@ package slack import ( - "math" "math/rand" "time" ) @@ -14,41 +13,42 @@ import ( // conjunction with the time package. type backoff struct { attempts int - //Factor is the multiplying factor for each increment step - Factor float64 - //Jitter eases contention by randomizing backoff steps - Jitter bool - //Min and Max are the minimum and maximum values of the counter - Min, Max time.Duration + // 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() time.Duration { - //Zero-values are nonsensical, so we use - //them to apply defaults - if b.Min == 0 { - b.Min = 100 * time.Millisecond - } +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.Factor == 0 { - b.Factor = 2 + + if b.Initial == 0 { + b.Initial = 100 * time.Millisecond } - //calculate this duration - dur := float64(b.Min) * math.Pow(b.Factor, float64(b.attempts)) - if b.Jitter { - dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min) + + // calculate this duration + if dur = time.Duration(1 << uint(b.attempts)); dur > 0 { + dur = dur * b.Initial + } else { + dur = b.Max } - //cap! - if dur > float64(b.Max) { - return b.Max + + if b.Jitter > 0 { + dur = dur + time.Duration(rand.Intn(int(b.Jitter))) } - //bump attempts count + + // bump attempts count b.attempts++ - //return as a time.Duration - return time.Duration(dur) + + return dur } //Resets the current value of the counter back to Min diff --git a/vendor/github.com/nlopes/slack/block.go b/vendor/github.com/nlopes/slack/block.go new file mode 100644 index 00000000..1fc7fece --- /dev/null +++ b/vendor/github.com/nlopes/slack/block.go @@ -0,0 +1,71 @@ +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" +) + +// 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"` + SelectedUser string `json:"selected_user"` + SelectedChannel string `json:"selected_channel"` + SelectedConversation string `json:"selected_conversation"` + SelectedDate string `json:"selected_date"` + 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"` +} + +// 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/vendor/github.com/nlopes/slack/block_action.go b/vendor/github.com/nlopes/slack/block_action.go new file mode 100644 index 00000000..fe46a95c --- /dev/null +++ b/vendor/github.com/nlopes/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/vendor/github.com/nlopes/slack/block_context.go b/vendor/github.com/nlopes/slack/block_context.go new file mode 100644 index 00000000..c37bf27e --- /dev/null +++ b/vendor/github.com/nlopes/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/vendor/github.com/nlopes/slack/block_conv.go b/vendor/github.com/nlopes/slack/block_conv.go new file mode 100644 index 00000000..619867ea --- /dev/null +++ b/vendor/github.com/nlopes/slack/block_conv.go @@ -0,0 +1,303 @@ +package slack + +import ( + "encoding/json" + + "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 "image": + block = &ImageBlock{} + case "section": + block = &SectionBlock{} + default: + return errors.New("unsupported block type") + } + + err = json.Unmarshal(r, block) + if err != nil { + return err + } + + blocks.BlockSet = append(blocks.BlockSet, block) + } + + *b = blocks + 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 "static_select", "external_select", "users_select", "conversations_select", "channels_select": + blockElement = &SelectBlockElement{} + default: + return errors.New("unsupported block element type") + } + + 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 "static_select": + element, err := unmarshalBlockElement(r, &SelectBlockElement{}) + if err != nil { + return err + } + a.SelectElement = element.(*SelectBlockElement) + } + + 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.SelectElement != nil { + return element.SelectElement + } + + 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/vendor/github.com/nlopes/slack/block_divider.go b/vendor/github.com/nlopes/slack/block_divider.go new file mode 100644 index 00000000..2d442ba1 --- /dev/null +++ b/vendor/github.com/nlopes/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/vendor/github.com/nlopes/slack/block_element.go b/vendor/github.com/nlopes/slack/block_element.go new file mode 100644 index 00000000..c62ba99c --- /dev/null +++ b/vendor/github.com/nlopes/slack/block_element.go @@ -0,0 +1,238 @@ +package slack + +// https://api.slack.com/reference/messaging/block-elements + +const ( + METImage MessageElementType = "image" + METButton MessageElementType = "button" + METOverflow MessageElementType = "overflow" + METDatepicker MessageElementType = "datepicker" + + 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" +) + +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 + SelectElement *SelectBlockElement +} + +// 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 *SelectBlockElement: + return &Accessory{SelectElement: element.(*SelectBlockElement)} + } + + return nil +} + +// 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"` +} + +// 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, + } +} + +type Style string + +const ( + StyleDefault Style = "default" + 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 +} + +// add styling to button object +func (s *ButtonBlockElement) WithStyle(style Style) { + s.Style = style +} + +// 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, + } +} + +// 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"` + MinQueryLength int `json:"min_query_length,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,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, + } +} + +// 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"` + 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, + } +} diff --git a/vendor/github.com/nlopes/slack/block_image.go b/vendor/github.com/nlopes/slack/block_image.go new file mode 100644 index 00000000..6de3f63a --- /dev/null +++ b/vendor/github.com/nlopes/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"` +} + +// 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/vendor/github.com/nlopes/slack/block_object.go b/vendor/github.com/nlopes/slack/block_object.go new file mode 100644 index 00000000..9e77e6c7 --- /dev/null +++ b/vendor/github.com/nlopes/slack/block_object.go @@ -0,0 +1,216 @@ +package slack + +import ( + "encoding/json" +) + +// 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 +} + +// 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, + } +} + +// 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"` +} + +// validateType enforces block objects for element and block parameters +func (s ConfirmationBlockObject) validateType() MessageObjectType { + return motConfirmation +} + +// 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"` + URL string `json:"url"` +} + +// NewOptionBlockObject returns an instance of a new Option Block Element +func NewOptionBlockObject(value string, text *TextBlockObject) *OptionBlockObject { + return &OptionBlockObject{ + Text: text, + Value: value, + } +} + +// 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/vendor/github.com/nlopes/slack/block_section.go b/vendor/github.com/nlopes/slack/block_section.go new file mode 100644 index 00000000..01ffd5a1 --- /dev/null +++ b/vendor/github.com/nlopes/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/vendor/github.com/nlopes/slack/bots.go b/vendor/github.com/nlopes/slack/bots.go index 92570a04..5d5a2add 100644 --- a/vendor/github.com/nlopes/slack/bots.go +++ b/vendor/github.com/nlopes/slack/bots.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" ) @@ -19,15 +18,17 @@ type botResponseFull struct { SlackResponse } -func botRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*botResponseFull, error) { +func (api *Client) botRequest(ctx context.Context, path string, values url.Values) (*botResponseFull, error) { response := &botResponseFull{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) + + if err := response.Err(); err != nil { + return nil, err } + return response, nil } @@ -40,10 +41,13 @@ func (api *Client) GetBotInfo(bot string) (*Bot, error) { func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) { values := url.Values{ "token": {api.token}, - "bot": {bot}, } - response, err := botRequest(ctx, api.httpclient, "bots.info", values, api.debug) + if bot != "" { + values.Add("bot", bot) + } + + response, err := api.botRequest(ctx, "bots.info", values) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/channels.go b/vendor/github.com/nlopes/slack/channels.go index 6204315a..c99e6655 100644 --- a/vendor/github.com/nlopes/slack/channels.go +++ b/vendor/github.com/nlopes/slack/channels.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strconv" ) @@ -19,23 +18,44 @@ type channelResponseFull struct { // Channel contains information about the channel type Channel struct { - groupConversation + GroupConversation IsChannel bool `json:"is_channel"` IsGeneral bool `json:"is_general"` IsMember bool `json:"is_member"` Locale string `json:"locale"` } -func channelRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*channelResponseFull, error) { +func (api *Client) channelRequest(ctx context.Context, path string, values url.Values) (*channelResponseFull, error) { response := &channelResponseFull{} - err := postForm(ctx, client, SLACK_API+path, values, response, debug) + err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) + + return response, response.Err() +} + +type channelsConfig struct { + values url.Values +} + +// GetChannelsOption option provided when getting channels. +type GetChannelsOption func(*channelsConfig) error + +// GetChannelsOptionExcludeMembers excludes the members collection from each channel. +func GetChannelsOptionExcludeMembers() GetChannelsOption { + return func(config *channelsConfig) error { + config.values.Add("exclude_members", "true") + return nil + } +} + +// GetChannelsOptionExcludeArchived excludes archived channels from results. +func GetChannelsOptionExcludeArchived() GetChannelsOption { + return func(config *channelsConfig) error { + config.values.Add("exclude_archived", "true") + return nil } - return response, nil } // ArchiveChannel archives the given channel @@ -52,7 +72,7 @@ func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) "channel": {channelID}, } - _, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug) + _, err = api.channelRequest(ctx, "channels.archive", values) return err } @@ -70,7 +90,7 @@ func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string "channel": {channelID}, } - _, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug) + _, err = api.channelRequest(ctx, "channels.unarchive", values) return err } @@ -88,7 +108,7 @@ func (api *Client) CreateChannelContext(ctx context.Context, channelName string) "name": {channelName}, } - response, err := channelRequest(ctx, api.httpclient, "channels.create", values, api.debug) + response, err := api.channelRequest(ctx, "channels.create", values) if err != nil { return nil, err } @@ -133,7 +153,7 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID strin } } - response, err := channelRequest(ctx, api.httpclient, "channels.history", values, api.debug) + response, err := api.channelRequest(ctx, "channels.history", values) if err != nil { return nil, err } @@ -150,11 +170,12 @@ func (api *Client) GetChannelInfo(channelID string) (*Channel, error) { // see https://api.slack.com/methods/channels.info func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) { values := url.Values{ - "token": {api.token}, - "channel": {channelID}, + "token": {api.token}, + "channel": {channelID}, + "include_locale": {strconv.FormatBool(true)}, } - response, err := channelRequest(ctx, api.httpclient, "channels.info", values, api.debug) + response, err := api.channelRequest(ctx, "channels.info", values) if err != nil { return nil, err } @@ -167,7 +188,7 @@ func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error) return api.InviteUserToChannelContext(context.Background(), channelID, user) } -// InviteUserToChannelCustom invites a user to a given channel and returns a *Channel with a custom context +// InviteUserToChannelContext invites a user to a given channel and returns a *Channel with a custom context // see https://api.slack.com/methods/channels.invite func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) { values := url.Values{ @@ -176,7 +197,7 @@ func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, us "user": {user}, } - response, err := channelRequest(ctx, api.httpclient, "channels.invite", values, api.debug) + response, err := api.channelRequest(ctx, "channels.invite", values) if err != nil { return nil, err } @@ -197,7 +218,7 @@ func (api *Client) JoinChannelContext(ctx context.Context, channelName string) ( "name": {channelName}, } - response, err := channelRequest(ctx, api.httpclient, "channels.join", values, api.debug) + response, err := api.channelRequest(ctx, "channels.join", values) if err != nil { return nil, err } @@ -218,7 +239,7 @@ func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (b "channel": {channelID}, } - response, err := channelRequest(ctx, api.httpclient, "channels.leave", values, api.debug) + response, err := api.channelRequest(ctx, "channels.leave", values) if err != nil { return false, err } @@ -241,27 +262,36 @@ func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, us "user": {user}, } - _, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug) + _, err = api.channelRequest(ctx, "channels.kick", values) return err } // GetChannels retrieves all the channels // see https://api.slack.com/methods/channels.list -func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) { - return api.GetChannelsContext(context.Background(), excludeArchived) +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 -func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) ([]Channel, error) { - values := url.Values{ - "token": {api.token}, +func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool, options ...GetChannelsOption) ([]Channel, error) { + config := channelsConfig{ + values: url.Values{ + "token": {api.token}, + }, } + if excludeArchived { - values.Add("exclude_archived", "1") + options = append(options, GetChannelsOptionExcludeArchived()) + } + + for _, opt := range options { + if err := opt(&config); err != nil { + return nil, err + } } - response, err := channelRequest(ctx, api.httpclient, "channels.list", values, api.debug) + response, err := api.channelRequest(ctx, "channels.list", config.values) if err != nil { return nil, err } @@ -288,7 +318,7 @@ func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts "ts": {ts}, } - _, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug) + _, err = api.channelRequest(ctx, "channels.mark", values) return err } @@ -309,7 +339,7 @@ func (api *Client) RenameChannelContext(ctx context.Context, channelID, name str // 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 := channelRequest(ctx, api.httpclient, "channels.rename", values, api.debug) + response, err := api.channelRequest(ctx, "channels.rename", values) if err != nil { return nil, err } @@ -331,7 +361,7 @@ func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purp "purpose": {purpose}, } - response, err := channelRequest(ctx, api.httpclient, "channels.setPurpose", values, api.debug) + response, err := api.channelRequest(ctx, "channels.setPurpose", values) if err != nil { return "", err } @@ -353,7 +383,7 @@ func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic "topic": {topic}, } - response, err := channelRequest(ctx, api.httpclient, "channels.setTopic", values, api.debug) + response, err := api.channelRequest(ctx, "channels.setTopic", values) if err != nil { return "", err } @@ -374,7 +404,7 @@ func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thre "channel": {channelID}, "thread_ts": {thread_ts}, } - response, err := channelRequest(ctx, api.httpclient, "channels.replies", values, api.debug) + response, err := api.channelRequest(ctx, "channels.replies", values) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/chat.go b/vendor/github.com/nlopes/slack/chat.go index 78b68cc6..a480e5a7 100644 --- a/vendor/github.com/nlopes/slack/chat.go +++ b/vendor/github.com/nlopes/slack/chat.go @@ -3,6 +3,7 @@ package slack import ( "context" "encoding/json" + "net/http" "net/url" "github.com/nlopes/slack/slackutilsx" @@ -25,7 +26,7 @@ const ( type chatResponseFull struct { Channel string `json:"channel"` - Timestamp string `json:"ts"` //Regualr message timestamp + Timestamp string `json:"ts"` //Regular message timestamp MessageTimeStamp string `json:"message_ts"` //Ephemeral message timestamp Text string `json:"text"` SlackResponse @@ -43,19 +44,18 @@ func (c chatResponseFull) getMessageTimestamp() string { // 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"` - Attachments []Attachment `json:"attachments"` - 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"` + 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"` @@ -71,7 +71,6 @@ func NewPostMessageParameters() PostMessageParameters { Parse: DEFAULT_MESSAGE_PARSE, ThreadTimestamp: DEFAULT_MESSAGE_THREAD_TIMESTAMP, LinkNames: DEFAULT_MESSAGE_LINK_NAMES, - Attachments: nil, UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS, UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA, IconURL: DEFAULT_MESSAGE_ICON_URL, @@ -96,26 +95,24 @@ func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTim // 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(channel, text string, params PostMessageParameters) (string, string, error) { +func (api *Client) PostMessage(channelID string, options ...MsgOption) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext( context.Background(), - channel, - MsgOptionText(text, params.EscapeText), - MsgOptionAttachments(params.Attachments...), - MsgOptionPostMessageParameters(params), + 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, channel, text string, params PostMessageParameters) (string, string, error) { +// 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, - channel, - MsgOptionText(text, params.EscapeText), - MsgOptionAttachments(params.Attachments...), - MsgOptionPostMessageParameters(params), + channelID, + MsgOptionPost(), + MsgOptionCompose(options...), ) return respChannel, respTimestamp, err } @@ -135,18 +132,23 @@ func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) // 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, append(options, MsgOptionPostEphemeral2(userID))...) + _, 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, text string) (string, string, string, error) { - return api.UpdateMessageContext(context.Background(), channelID, timestamp, text) +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, text string) (string, string, string, error) { - return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionText(text, true)) +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...)) } // SendMessage more flexible method for configuring messages. @@ -155,32 +157,36 @@ func (api *Client) SendMessage(channel string, options ...MsgOption) (string, st } // 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) { +func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestamp string, _text string, err error) { var ( - config sendConfig + req *http.Request + parser func(*chatResponseFull) responseParser response chatResponseFull ) - if config, err = applyMsgOptions(api.token, channelID, options...); err != nil { + if req, parser, err = buildSender(api.endpoint, options...).BuildRequest(api.token, channelID); err != nil { return "", "", "", err } - if err = postSlackMethod(ctx, api.httpclient, string(config.mode), config.values, &response, api.debug); err != nil { + if err = doPost(ctx, api.httpclient, req, parser(&response), api); err != nil { return "", "", "", err } return response.Channel, response.getMessageTimestamp(), response.Text, response.Err() } -// ApplyMsgOptions utility function for debugging/testing chat requests. -func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) { - config, err := applyMsgOptions(token, channel, options...) - return string(config.mode), config.values, 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 string, options ...MsgOption) (sendConfig, error) { +func applyMsgOptions(token, channel, apiurl string, options ...MsgOption) (sendConfig, error) { config := sendConfig{ - mode: chatPostMessage, + apiurl: apiurl, + endpoint: apiurl + string(chatPostMessage), values: url.Values{ "token": {token}, "channel": {channel}, @@ -196,6 +202,13 @@ func applyMsgOptions(token, channel string, options ...MsgOption) (sendConfig, e return config, nil } +func buildSender(apiurl string, options ...MsgOption) sendConfig { + return sendConfig{ + apiurl: apiurl, + options: options, + } +} + type sendMode string const ( @@ -203,12 +216,68 @@ const ( chatPostMessage sendMode = "chat.postMessage" chatDelete sendMode = "chat.delete" chatPostEphemeral sendMode = "chat.postEphemeral" + chatResponse sendMode = "chat.responseURL" chatMeMessage sendMode = "chat.meMessage" + chatUnfurl sendMode = "chat.unfurl" ) type sendConfig struct { - mode sendMode - values url.Values + apiurl string + options []MsgOption + mode sendMode + endpoint string + values url.Values + attachments []Attachment + responseType string +} + +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, + responseType: t.responseType, + }.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 + responseType string +} + +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, + ResponseType: t.responseType, + }) + return req, func(resp *chatResponseFull) responseParser { + return newContentTypeParser(resp) + }, err } // MsgOption option provided when sending a message. @@ -217,26 +286,16 @@ type MsgOption func(*sendConfig) error // MsgOptionPost posts a messages, this is the default. func MsgOptionPost() MsgOption { return func(config *sendConfig) error { - config.mode = chatPostMessage - config.values.Del("ts") - return nil - } -} - -// MsgOptionPostEphemeral - DEPRECATED: use MsgOptionPostEphemeral2 -// posts an ephemeral message. -func MsgOptionPostEphemeral() MsgOption { - return func(config *sendConfig) error { - config.mode = chatPostEphemeral + config.endpoint = config.apiurl + string(chatPostMessage) config.values.Del("ts") return nil } } -// MsgOptionPostEphemeral2 - posts an ephemeral message to the provided user. -func MsgOptionPostEphemeral2(userID string) MsgOption { +// MsgOptionPostEphemeral - posts an ephemeral message to the provided user. +func MsgOptionPostEphemeral(userID string) MsgOption { return func(config *sendConfig) error { - config.mode = chatPostEphemeral + config.endpoint = config.apiurl + string(chatPostEphemeral) MsgOptionUser(userID)(config) config.values.Del("ts") @@ -247,7 +306,7 @@ func MsgOptionPostEphemeral2(userID string) MsgOption { // MsgOptionMeMessage posts a "me message" type from the calling user func MsgOptionMeMessage() MsgOption { return func(config *sendConfig) error { - config.mode = chatMeMessage + config.endpoint = config.apiurl + string(chatMeMessage) return nil } } @@ -255,7 +314,7 @@ func MsgOptionMeMessage() MsgOption { // MsgOptionUpdate updates a message based on the timestamp. func MsgOptionUpdate(timestamp string) MsgOption { return func(config *sendConfig) error { - config.mode = chatUpdate + config.endpoint = config.apiurl + string(chatUpdate) config.values.Add("ts", timestamp) return nil } @@ -264,8 +323,32 @@ func MsgOptionUpdate(timestamp string) MsgOption { // MsgOptionDelete deletes a message based on the timestamp. func MsgOptionDelete(timestamp string) MsgOption { return func(config *sendConfig) error { - config.mode = chatDelete + 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 + } +} + +// MsgOptionResponseURL supplies a url to use as the endpoint. +func MsgOptionResponseURL(url string, rt string) MsgOption { + return func(config *sendConfig) error { + config.mode = chatResponse + config.endpoint = url + config.responseType = rt + config.values.Del("ts") return nil } } @@ -288,6 +371,14 @@ func MsgOptionUser(userID string) MsgOption { } } +// 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 { @@ -307,9 +398,31 @@ func MsgOptionAttachments(attachments ...Attachment) MsgOption { return nil } - attachments, err := json.Marshal(attachments) + 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 + } + + blocks, err := json.Marshal(blocks) if err == nil { - config.values.Set("attachments", string(attachments)) + config.values.Set("blocks", string(blocks)) } return err } @@ -363,7 +476,7 @@ func MsgOptionBroadcast() MsgOption { } } -// this function combines multiple options into a single option. +// MsgOptionCompose combines multiple options into a single option. func MsgOptionCompose(options ...MsgOption) MsgOption { return func(c *sendConfig) error { for _, opt := range options { @@ -375,19 +488,48 @@ func MsgOptionCompose(options ...MsgOption) MsgOption { } } +// MsgOptionParse set parse option. func MsgOptionParse(b bool) MsgOption { return func(c *sendConfig) error { var v string if b { - v = "1" + v = "full" } else { - v = "0" + v = "none" } c.values.Set("parse", v) return nil } } +// MsgOptionIconURL sets an icon URL +func MsgOptionIconURL(iconURL string) MsgOption { + return func(c *sendConfig) error { + c.values.Set("icon_url", iconURL) + return nil + } +} + +// MsgOptionIconEmoji sets an icon emoji +func MsgOptionIconEmoji(iconEmoji string) MsgOption { + return func(c *sendConfig) error { + c.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 { @@ -442,3 +584,38 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { 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() +} diff --git a/vendor/github.com/nlopes/slack/conversation.go b/vendor/github.com/nlopes/slack/conversation.go index 9e113e0d..1e4a61f1 100644 --- a/vendor/github.com/nlopes/slack/conversation.go +++ b/vendor/github.com/nlopes/slack/conversation.go @@ -2,14 +2,13 @@ package slack import ( "context" - "errors" "net/url" "strconv" "strings" ) // Conversation is the foundation for IM and BaseGroupConversation -type conversation struct { +type Conversation struct { ID string `json:"id"` Created JSONTime `json:"created"` IsOpen bool `json:"is_open"` @@ -36,8 +35,8 @@ type conversation struct { } // GroupConversation is the foundation for Group and Channel -type groupConversation struct { - conversation +type GroupConversation struct { + Conversation Name string `json:"name"` Creator string `json:"creator"` IsArchived bool `json:"is_archived"` @@ -66,6 +65,14 @@ type GetUsersInConversationParameters struct { Limit int } +type GetConversationsForUserParameters struct { + UserID string + Cursor string + Types []string + Limit int + ExcludeArchived bool +} + type responseMetaData struct { NextCursor string `json:"next_cursor"` } @@ -92,16 +99,57 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.members", values, &response, api.debug) + + err := api.postMethod(ctx, "conversations.members", values, &response) if err != nil { return nil, "", err } - if !response.Ok { - return nil, "", errors.New(response.Error) + + 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) @@ -113,8 +161,9 @@ func (api *Client) ArchiveConversationContext(ctx context.Context, channelID str "token": {api.token}, "channel": {channelID}, } + response := SlackResponse{} - err := postSlackMethod(ctx, api.httpclient, "conversations.archive", values, &response, api.debug) + err := api.postMethod(ctx, "conversations.archive", values, &response) if err != nil { return err } @@ -134,7 +183,7 @@ func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID s "channel": {channelID}, } response := SlackResponse{} - err := postSlackMethod(ctx, api.httpclient, "conversations.unarchive", values, &response, api.debug) + err := api.postMethod(ctx, "conversations.unarchive", values, &response) if err != nil { return err } @@ -158,7 +207,7 @@ func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID, SlackResponse Channel *Channel `json:"channel"` }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.setTopic", values, &response, api.debug) + err := api.postMethod(ctx, "conversations.setTopic", values, &response) if err != nil { return nil, err } @@ -182,7 +231,8 @@ func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelI SlackResponse Channel *Channel `json:"channel"` }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.setPurpose", values, &response, api.debug) + + err := api.postMethod(ctx, "conversations.setPurpose", values, &response) if err != nil { return nil, err } @@ -206,7 +256,8 @@ func (api *Client) RenameConversationContext(ctx context.Context, channelID, cha SlackResponse Channel *Channel `json:"channel"` }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.rename", values, &response, api.debug) + + err := api.postMethod(ctx, "conversations.rename", values, &response) if err != nil { return nil, err } @@ -230,7 +281,8 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel SlackResponse Channel *Channel `json:"channel"` }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.invite", values, &response, api.debug) + + err := api.postMethod(ctx, "conversations.invite", values, &response) if err != nil { return nil, err } @@ -250,8 +302,9 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI "channel": {channelID}, "user": {user}, } + response := SlackResponse{} - err := postSlackMethod(ctx, api.httpclient, "conversations.kick", values, &response, api.debug) + err := api.postMethod(ctx, "conversations.kick", values, &response) if err != nil { return err } @@ -276,7 +329,7 @@ func (api *Client) CloseConversationContext(ctx context.Context, channelID strin AlreadyClosed bool `json:"already_closed"` }{} - err = postSlackMethod(ctx, api.httpclient, "conversations.close", values, &response, api.debug) + err = api.postMethod(ctx, "conversations.close", values, &response) if err != nil { return false, false, err } @@ -296,13 +349,12 @@ func (api *Client) CreateConversationContext(ctx context.Context, channelName st "name": {channelName}, "is_private": {strconv.FormatBool(isPrivate)}, } - response, err := channelRequest( - ctx, api.httpclient, "conversations.create", values, api.debug) + response, err := api.channelRequest(ctx, "conversations.create", values) if err != nil { return nil, err } - return &response.Channel, response.Err() + return &response.Channel, nil } // GetConversationInfo retrieves information about a conversation @@ -317,8 +369,7 @@ func (api *Client) GetConversationInfoContext(ctx context.Context, channelID str "channel": {channelID}, "include_locale": {strconv.FormatBool(includeLocale)}, } - response, err := channelRequest( - ctx, api.httpclient, "conversations.info", values, api.debug) + response, err := api.channelRequest(ctx, "conversations.info", values) if err != nil { return nil, err } @@ -338,7 +389,7 @@ func (api *Client) LeaveConversationContext(ctx context.Context, channelID strin "channel": {channelID}, } - response, err := channelRequest(ctx, api.httpclient, "conversations.leave", values, api.debug) + response, err := api.channelRequest(ctx, "conversations.leave", values) if err != nil { return false, err } @@ -394,7 +445,7 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge Messages []Message `json:"messages"` }{} - err = postSlackMethod(ctx, api.httpclient, "conversations.replies", values, &response, api.debug) + err = api.postMethod(ctx, "conversations.replies", values, &response) if err != nil { return nil, false, "", err } @@ -434,7 +485,8 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} - err = postSlackMethod(ctx, api.httpclient, "conversations.list", values, &response, api.debug) + + err = api.postMethod(ctx, "conversations.list", values, &response) if err != nil { return nil, "", err } @@ -471,7 +523,8 @@ func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConv AlreadyOpen bool `json:"already_open"` SlackResponse }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.open", values, &response, api.debug) + + err := api.postMethod(ctx, "conversations.open", values, &response) if err != nil { return nil, false, false, err } @@ -495,7 +548,8 @@ func (api *Client) JoinConversationContext(ctx context.Context, channelID string } `json:"response_metadata"` SlackResponse }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.join", values, &response, api.debug) + + err := api.postMethod(ctx, "conversations.join", values, &response) if err != nil { return nil, "", nil, err } @@ -557,12 +611,10 @@ func (api *Client) GetConversationHistoryContext(ctx context.Context, params *Ge response := GetConversationHistoryResponse{} - err := postSlackMethod(ctx, api.httpclient, "conversations.history", values, &response, api.debug) + err := api.postMethod(ctx, "conversations.history", values, &response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return &response, nil + + return &response, response.Err() } diff --git a/vendor/github.com/nlopes/slack/dialog.go b/vendor/github.com/nlopes/slack/dialog.go index d1435d54..376cd9e6 100644 --- a/vendor/github.com/nlopes/slack/dialog.go +++ b/vendor/github.com/nlopes/slack/dialog.go @@ -3,7 +3,7 @@ package slack import ( "context" "encoding/json" - "errors" + "strings" ) // InputType is the type of the dialog input type @@ -14,7 +14,7 @@ const ( InputTypeText InputType = "text" // InputTypeTextArea textarea input InputTypeTextArea InputType = "textarea" - // InputTypeSelect textfield input + // InputTypeSelect select menus input InputTypeSelect InputType = "select" ) @@ -25,6 +25,7 @@ type DialogInput struct { Name string `json:"name"` Placeholder string `json:"placeholder"` Optional bool `json:"optional"` + Hint string `json:"hint"` } // DialogTrigger ... @@ -36,8 +37,9 @@ type DialogTrigger struct { // 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 + 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"` @@ -47,30 +49,13 @@ type Dialog struct { // DialogElement abstract type for dialogs. type DialogElement interface{} -// DialogCallback is sent from Slack when a user submits a form from within a dialog -type DialogCallback struct { - Type string `json:"type"` - CallbackID string `json:"callback_id"` - Team Team `json:"team"` - Channel Channel `json:"channel"` - User User `json:"user"` - ActionTs string `json:"action_ts"` - Token string `json:"token"` - ResponseURL string `json:"response_url"` - Submission map[string]string `json:"submission"` -} +// DialogCallback DEPRECATED use InteractionCallback +type DialogCallback InteractionCallback -// DialogSuggestionCallback is sent from Slack when a user types in a select field with an external data source -type DialogSuggestionCallback struct { - Type string `json:"type"` - Token string `json:"token"` - ActionTs string `json:"action_ts"` - Team Team `json:"team"` - User User `json:"user"` - Channel Channel `json:"channel"` - ElementName string `json:"name"` - Value string `json:"value"` - CallbackID string `json:"callback_id"` +// DialogSubmissionCallback is sent from Slack when a user submits a form from within a dialog +type DialogSubmissionCallback struct { + State string `json:"state,omitempty"` + Submission map[string]string `json:"submission"` } // DialogOpenResponse response from `dialog.open` @@ -84,6 +69,17 @@ 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) { @@ -94,7 +90,7 @@ func (api *Client) OpenDialog(triggerID string, dialog Dialog) (err error) { // 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 errors.New("received empty parameters") + return ErrParametersMissing } req := DialogTrigger{ @@ -108,10 +104,15 @@ func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dial } response := &DialogOpenResponse{} - endpoint := SLACK_API + "dialog.open" - if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api.debug); err != nil { + 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/vendor/github.com/nlopes/slack/dialog_select.go b/vendor/github.com/nlopes/slack/dialog_select.go index cff35479..385cef68 100644 --- a/vendor/github.com/nlopes/slack/dialog_select.go +++ b/vendor/github.com/nlopes/slack/dialog_select.go @@ -21,9 +21,11 @@ type DialogInputSelect struct { DialogInput Value string `json:"value,omitempty"` //Optional. DataSource SelectDataSource `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external". - SelectedOptions string `json:"selected_options,omitempty"` //Optional. Default value for "external" only + 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 @@ -53,14 +55,7 @@ func NewStaticSelectDialogInput(name, label string, options []DialogSelectOption } // NewGroupedSelectDialogInput creates grouped options select input for Dialogs. -func NewGroupedSelectDialogInput(name, label string, groups map[string]map[string]string) *DialogInputSelect { - optionGroups := []DialogOptionGroup{} - for groupName, options := range groups { - optionGroups = append(optionGroups, DialogOptionGroup{ - Label: groupName, - Options: optionsFromMap(options), - }) - } +func NewGroupedSelectDialogInput(name, label string, options []DialogOptionGroup) *DialogInputSelect { return &DialogInputSelect{ DialogInput: DialogInput{ Type: InputTypeSelect, @@ -68,34 +63,15 @@ func NewGroupedSelectDialogInput(name, label string, groups map[string]map[strin Label: label, }, DataSource: DialogDataSourceStatic, - OptionGroups: optionGroups, - } -} - -func optionsFromArray(options []string) []DialogSelectOption { - selectOptions := make([]DialogSelectOption, len(options)) - for idx, value := range options { - selectOptions[idx] = DialogSelectOption{ - Label: value, - Value: value, - } - } - return selectOptions + OptionGroups: options} } -func optionsFromMap(options map[string]string) []DialogSelectOption { - selectOptions := make([]DialogSelectOption, len(options)) - idx := 0 - var option DialogSelectOption - for key, value := range options { - option = DialogSelectOption{ - Label: key, - Value: value, - } - selectOptions[idx] = option - idx++ +// NewDialogOptionGroup creates a DialogOptionGroup from several select options +func NewDialogOptionGroup(label string, options ...DialogSelectOption) DialogOptionGroup { + return DialogOptionGroup{ + Label: label, + Options: options, } - return selectOptions } // NewConversationsSelect returns a `Conversations` select diff --git a/vendor/github.com/nlopes/slack/dialog_text.go b/vendor/github.com/nlopes/slack/dialog_text.go index bf9602cc..da06bd6d 100644 --- a/vendor/github.com/nlopes/slack/dialog_text.go +++ b/vendor/github.com/nlopes/slack/dialog_text.go @@ -3,6 +3,9 @@ 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" @@ -26,8 +29,8 @@ type TextInputElement struct { } // NewTextInput constructor for a `text` input -func NewTextInput(name, label, text string) *TextInputElement { - return &TextInputElement{ +func NewTextInput(name, label, text string, options ...TextInputOption) *TextInputElement { + t := &TextInputElement{ DialogInput: DialogInput{ Type: InputTypeText, Name: name, @@ -35,6 +38,12 @@ func NewTextInput(name, label, text string) *TextInputElement { }, Value: text, } + + for _, opt := range options { + opt(t) + } + + return t } // NewTextAreaInput constructor for a `textarea` input diff --git a/vendor/github.com/nlopes/slack/dnd.go b/vendor/github.com/nlopes/slack/dnd.go index 26d36d6a..a3aa680c 100644 --- a/vendor/github.com/nlopes/slack/dnd.go +++ b/vendor/github.com/nlopes/slack/dnd.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strconv" "strings" @@ -36,16 +35,14 @@ type dndTeamInfoResponse struct { SlackResponse } -func dndRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*dndResponseFull, error) { +func (api *Client) dndRequest(ctx context.Context, path string, values url.Values) (*dndResponseFull, error) { response := &dndResponseFull{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } // EndDND ends the user's scheduled Do Not Disturb session @@ -61,7 +58,7 @@ func (api *Client) EndDNDContext(ctx context.Context) error { response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "dnd.endDnd", values, response, api.debug); err != nil { + if err := api.postMethod(ctx, "dnd.endDnd", values, response); err != nil { return err } @@ -79,7 +76,7 @@ func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { "token": {api.token}, } - response, err := dndRequest(ctx, api.httpclient, "dnd.endSnooze", values, api.debug) + response, err := api.dndRequest(ctx, "dnd.endSnooze", values) if err != nil { return nil, err } @@ -100,7 +97,7 @@ func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDSta values.Set("user", *user) } - response, err := dndRequest(ctx, api.httpclient, "dnd.info", values, api.debug) + response, err := api.dndRequest(ctx, "dnd.info", values) if err != nil { return nil, err } @@ -120,12 +117,14 @@ func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (m } response := &dndTeamInfoResponse{} - if err := postSlackMethod(ctx, api.httpclient, "dnd.teamInfo", values, response, api.debug); err != nil { + if err := api.postMethod(ctx, "dnd.teamInfo", values, response); err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) + + if response.Err() != nil { + return nil, response.Err() } + return response.Users, nil } @@ -136,7 +135,7 @@ func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) { return api.SetSnoozeContext(context.Background(), minutes) } -// SetSnooze adjusts the snooze duration for a user's Do Not Disturb settings with a custom context. +// 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{ @@ -144,7 +143,7 @@ func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatu "num_minutes": {strconv.Itoa(minutes)}, } - response, err := dndRequest(ctx, api.httpclient, "dnd.setSnooze", values, api.debug) + response, err := api.dndRequest(ctx, "dnd.setSnooze", values) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/emoji.go b/vendor/github.com/nlopes/slack/emoji.go index fe2945c4..b2b0c6c9 100644 --- a/vendor/github.com/nlopes/slack/emoji.go +++ b/vendor/github.com/nlopes/slack/emoji.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" ) @@ -23,12 +22,14 @@ func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, erro } response := &emojiResponseFull{} - err := postSlackMethod(ctx, api.httpclient, "emoji.list", values, response, api.debug) + err := api.postMethod(ctx, "emoji.list", values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) + + if response.Err() != nil { + return nil, response.Err() } + return response.Emoji, nil } diff --git a/vendor/github.com/nlopes/slack/errors.go b/vendor/github.com/nlopes/slack/errors.go new file mode 100644 index 00000000..09113ff1 --- /dev/null +++ b/vendor/github.com/nlopes/slack/errors.go @@ -0,0 +1,18 @@ +package slack + +import "github.com/nlopes/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") + ErrParametersMissing = errorsx.String("received empty parameters") + 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/vendor/github.com/nlopes/slack/files.go b/vendor/github.com/nlopes/slack/files.go index 2381ec3c..3a7363de 100644 --- a/vendor/github.com/nlopes/slack/files.go +++ b/vendor/github.com/nlopes/slack/files.go @@ -2,7 +2,7 @@ package slack import ( "context" - "errors" + "fmt" "io" "net/url" "strconv" @@ -86,21 +86,41 @@ type File struct { 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 + 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 @@ -114,11 +134,21 @@ type GetFilesParameters struct { Page int } +// 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"` + Comments []Comment `json:"comments"` + Files []File `json:"files"` + Metadata ResponseMetadata `json:"response_metadata"` SlackResponse } @@ -136,16 +166,14 @@ func NewGetFilesParameters() GetFilesParameters { } } -func fileRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*fileResponseFull, error) { +func (api *Client) fileRequest(ctx context.Context, path string, values url.Values) (*fileResponseFull, error) { response := &fileResponseFull{} - err := postForm(ctx, client, SLACK_API+path, values, response, debug) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } // GetFileInfo retrieves a file and related comments @@ -162,18 +190,57 @@ func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, "page": {strconv.Itoa(page)}, } - response, err := fileRequest(ctx, api.httpclient, "files.info", values, api.debug) + 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{ @@ -201,7 +268,7 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter values.Add("page", strconv.Itoa(params.Page)) } - response, err := fileRequest(ctx, api.httpclient, "files.list", values, api.debug) + response, err := api.fileRequest(ctx, "files.list", values) if err != nil { return nil, nil, err } @@ -237,24 +304,29 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam 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 = postForm(ctx, api.httpclient, SLACK_API+"files.upload", values, response, api.debug) + err = api.postMethod(ctx, "files.upload", values, response) } else if params.File != "" { - err = postLocalWithMultipartResponse(ctx, api.httpclient, "files.upload", params.File, "file", values, response, api.debug) + err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", values, response, api) } else if params.Reader != nil { - err = postWithMultipartResponse(ctx, api.httpclient, "files.upload", params.Filename, "file", values, params.Reader, response, api.debug) + 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 } - if !response.Ok { - return nil, errors.New(response.Error) - } - return &response.File, nil + + return &response.File, response.Err() } // DeleteFileComment deletes a file's comment @@ -265,7 +337,7 @@ func (api *Client) DeleteFileComment(commentID, fileID string) error { // 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 errors.New("received empty parameters") + return ErrParametersMissing } values := url.Values{ @@ -273,7 +345,7 @@ func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, comment "file": {fileID}, "id": {commentID}, } - _, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug) + _, err = api.fileRequest(ctx, "files.comments.delete", values) return err } @@ -289,7 +361,7 @@ func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err er "file": {fileID}, } - _, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug) + _, err = api.fileRequest(ctx, "files.delete", values) return err } @@ -305,7 +377,7 @@ func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string "file": {fileID}, } - response, err := fileRequest(ctx, api.httpclient, "files.revokePublicURL", values, api.debug) + response, err := api.fileRequest(ctx, "files.revokePublicURL", values) if err != nil { return nil, err } @@ -324,7 +396,7 @@ func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) "file": {fileID}, } - response, err := fileRequest(ctx, api.httpclient, "files.sharedPublicURL", values, api.debug) + response, err := api.fileRequest(ctx, "files.sharedPublicURL", values) if err != nil { return nil, nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/groups.go b/vendor/github.com/nlopes/slack/groups.go index 67e78e99..23374869 100644 --- a/vendor/github.com/nlopes/slack/groups.go +++ b/vendor/github.com/nlopes/slack/groups.go @@ -2,14 +2,13 @@ package slack import ( "context" - "errors" "net/url" "strconv" ) // Group contains all the information for a group type Group struct { - groupConversation + GroupConversation IsGroup bool `json:"is_group"` } @@ -28,16 +27,14 @@ type groupResponseFull struct { SlackResponse } -func groupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*groupResponseFull, error) { +func (api *Client) groupRequest(ctx context.Context, path string, values url.Values) (*groupResponseFull, error) { response := &groupResponseFull{} - err := postForm(ctx, client, SLACK_API+path, values, response, debug) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } // ArchiveGroup archives a private group @@ -52,7 +49,7 @@ func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error "channel": {group}, } - _, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api.debug) + _, err := api.groupRequest(ctx, "groups.archive", values) return err } @@ -68,7 +65,7 @@ func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) erro "channel": {group}, } - _, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api.debug) + _, err := api.groupRequest(ctx, "groups.unarchive", values) return err } @@ -84,7 +81,7 @@ func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group "name": {group}, } - response, err := groupRequest(ctx, api.httpclient, "groups.create", values, api.debug) + response, err := api.groupRequest(ctx, "groups.create", values) if err != nil { return nil, err } @@ -109,32 +106,13 @@ func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (* "channel": {group}, } - response, err := groupRequest(ctx, api.httpclient, "groups.createChild", values, api.debug) + response, err := api.groupRequest(ctx, "groups.createChild", values) if err != nil { return nil, err } return &response.Group, nil } -// CloseGroup closes a private group -func (api *Client) CloseGroup(group string) (bool, bool, error) { - return api.CloseGroupContext(context.Background(), group) -} - -// CloseGroupContext closes a private group with a custom context -func (api *Client) CloseGroupContext(ctx context.Context, group string) (bool, bool, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - - response, err := imRequest(ctx, api.httpclient, "groups.close", values, api.debug) - if err != nil { - return false, false, err - } - return response.NoOp, response.AlreadyClosed, nil -} - // GetGroupHistory fetches all the history for a private group func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) { return api.GetGroupHistoryContext(context.Background(), group, params) @@ -170,7 +148,7 @@ func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, par } } - response, err := groupRequest(ctx, api.httpclient, "groups.history", values, api.debug) + response, err := api.groupRequest(ctx, "groups.history", values) if err != nil { return nil, err } @@ -190,7 +168,7 @@ func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user str "user": {user}, } - response, err := groupRequest(ctx, api.httpclient, "groups.invite", values, api.debug) + response, err := api.groupRequest(ctx, "groups.invite", values) if err != nil { return nil, false, err } @@ -209,7 +187,7 @@ func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err err "channel": {group}, } - _, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug) + _, err = api.groupRequest(ctx, "groups.leave", values) return err } @@ -226,7 +204,7 @@ func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user str "user": {user}, } - _, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug) + _, err = api.groupRequest(ctx, "groups.kick", values) return err } @@ -244,7 +222,7 @@ func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) ( values.Add("exclude_archived", "1") } - response, err := groupRequest(ctx, api.httpclient, "groups.list", values, api.debug) + response, err := api.groupRequest(ctx, "groups.list", values) if err != nil { return nil, err } @@ -259,11 +237,12 @@ func (api *Client) GetGroupInfo(group string) (*Group, error) { // GetGroupInfoContext retrieves the given group with a custom context func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) { values := url.Values{ - "token": {api.token}, - "channel": {group}, + "token": {api.token}, + "channel": {group}, + "include_locale": {strconv.FormatBool(true)}, } - response, err := groupRequest(ctx, api.httpclient, "groups.info", values, api.debug) + response, err := api.groupRequest(ctx, "groups.info", values) if err != nil { return nil, err } @@ -288,7 +267,7 @@ func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string "ts": {ts}, } - _, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug) + _, err = api.groupRequest(ctx, "groups.mark", values) return err } @@ -304,7 +283,7 @@ func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bo "channel": {group}, } - response, err := groupRequest(ctx, api.httpclient, "groups.open", values, api.debug) + response, err := api.groupRequest(ctx, "groups.open", values) if err != nil { return false, false, err } @@ -328,7 +307,7 @@ func (api *Client) RenameGroupContext(ctx context.Context, group, name string) ( // 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 := groupRequest(ctx, api.httpclient, "groups.rename", values, api.debug) + response, err := api.groupRequest(ctx, "groups.rename", values) if err != nil { return nil, err } @@ -348,7 +327,7 @@ func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose st "purpose": {purpose}, } - response, err := groupRequest(ctx, api.httpclient, "groups.setPurpose", values, api.debug) + response, err := api.groupRequest(ctx, "groups.setPurpose", values) if err != nil { return "", err } @@ -368,7 +347,7 @@ func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string "topic": {topic}, } - response, err := groupRequest(ctx, api.httpclient, "groups.setTopic", values, api.debug) + response, err := api.groupRequest(ctx, "groups.setTopic", values) if err != nil { return "", err } diff --git a/vendor/github.com/nlopes/slack/im.go b/vendor/github.com/nlopes/slack/im.go index fa8b0959..ee784fef 100644 --- a/vendor/github.com/nlopes/slack/im.go +++ b/vendor/github.com/nlopes/slack/im.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strconv" ) @@ -23,22 +22,18 @@ type imResponseFull struct { // IM contains information related to the Direct Message channel type IM struct { - conversation - IsIM bool `json:"is_im"` - User string `json:"user"` - IsUserDeleted bool `json:"is_user_deleted"` + Conversation + IsUserDeleted bool `json:"is_user_deleted"` } -func imRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*imResponseFull, error) { +func (api *Client) imRequest(ctx context.Context, path string, values url.Values) (*imResponseFull, error) { response := &imResponseFull{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } // CloseIMChannel closes the direct message channel @@ -53,7 +48,7 @@ func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (b "channel": {channel}, } - response, err := imRequest(ctx, api.httpclient, "im.close", values, api.debug) + response, err := api.imRequest(ctx, "im.close", values) if err != nil { return false, false, err } @@ -74,7 +69,7 @@ func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool, "user": {user}, } - response, err := imRequest(ctx, api.httpclient, "im.open", values, api.debug) + response, err := api.imRequest(ctx, "im.open", values) if err != nil { return false, false, "", err } @@ -94,7 +89,7 @@ func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) "ts": {ts}, } - _, err := imRequest(ctx, api.httpclient, "im.mark", values, api.debug) + _, err := api.imRequest(ctx, "im.mark", values) return err } @@ -133,7 +128,7 @@ func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, para } } - response, err := imRequest(ctx, api.httpclient, "im.history", values, api.debug) + response, err := api.imRequest(ctx, "im.history", values) if err != nil { return nil, err } @@ -151,7 +146,7 @@ func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) { "token": {api.token}, } - response, err := imRequest(ctx, api.httpclient, "im.list", values, api.debug) + response, err := api.imRequest(ctx, "im.list", values) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/info.go b/vendor/github.com/nlopes/slack/info.go index db8534c7..31f459f1 100644 --- a/vendor/github.com/nlopes/slack/info.go +++ b/vendor/github.com/nlopes/slack/info.go @@ -156,17 +156,12 @@ type Icons struct { Image72 string `json:"image_72,omitempty"` } -// Info contains various details about Users, Channels, Bots and the authenticated user. +// 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"` - Users []User `json:"users,omitempty"` - Channels []Channel `json:"channels,omitempty"` - Groups []Group `json:"groups,omitempty"` - Bots []Bot `json:"bots,omitempty"` - IMs []IM `json:"ims,omitempty"` + URL string `json:"url,omitempty"` + User *UserDetails `json:"self,omitempty"` + Team *Team `json:"team,omitempty"` } type infoResponseFull struct { @@ -174,52 +169,27 @@ type infoResponseFull struct { SlackResponse } -// GetBotByID returns a bot given a bot id +// GetBotByID is deprecated and returns nil func (info Info) GetBotByID(botID string) *Bot { - for _, bot := range info.Bots { - if bot.ID == botID { - return &bot - } - } return nil } -// GetUserByID returns a user given a user id +// GetUserByID is deprecated and returns nil func (info Info) GetUserByID(userID string) *User { - for _, user := range info.Users { - if user.ID == userID { - return &user - } - } return nil } -// GetChannelByID returns a channel given a channel id +// GetChannelByID is deprecated and returns nil func (info Info) GetChannelByID(channelID string) *Channel { - for _, channel := range info.Channels { - if channel.ID == channelID { - return &channel - } - } return nil } -// GetGroupByID returns a group given a group id +// GetGroupByID is deprecated and returns nil func (info Info) GetGroupByID(groupID string) *Group { - for _, group := range info.Groups { - if group.ID == groupID { - return &group - } - } return nil } -// GetIMByID returns an IM given an IM id +// GetIMByID is deprecated and returns nil func (info Info) GetIMByID(imID string) *IM { - for _, im := range info.IMs { - if im.ID == imID { - return &im - } - } return nil } diff --git a/vendor/github.com/nlopes/slack/interactions.go b/vendor/github.com/nlopes/slack/interactions.go new file mode 100644 index 00000000..5433463d --- /dev/null +++ b/vendor/github.com/nlopes/slack/interactions.go @@ -0,0 +1,98 @@ +package slack + +import ( + "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") +) + +// 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"` + DialogSubmissionCallback +} + +// 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 +} + +// 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)) + return nil + } + + 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/vendor/github.com/nlopes/slack/internal/errorsx/errorsx.go b/vendor/github.com/nlopes/slack/internal/errorsx/errorsx.go new file mode 100644 index 00000000..cb850577 --- /dev/null +++ b/vendor/github.com/nlopes/slack/internal/errorsx/errorsx.go @@ -0,0 +1,8 @@ +package errorsx + +// String representing an error, useful for declaring string constants as errors. +type String string + +func (t String) Error() string { + return string(t) +} diff --git a/vendor/github.com/nlopes/slack/internal/timex/timex.go b/vendor/github.com/nlopes/slack/internal/timex/timex.go new file mode 100644 index 00000000..40063f73 --- /dev/null +++ b/vendor/github.com/nlopes/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/vendor/github.com/nlopes/slack/logger.go b/vendor/github.com/nlopes/slack/logger.go index 501d1672..6a3533a9 100644 --- a/vendor/github.com/nlopes/slack/logger.go +++ b/vendor/github.com/nlopes/slack/logger.go @@ -2,52 +2,59 @@ package slack import ( "fmt" - "sync" ) -// SetLogger let's library users supply a logger, so that api debugging -// can be logged along with the application's debugging info. -func SetLogger(l logProvider) { - loggerMutex.Lock() - logger = ilogger{logProvider: l} - loggerMutex.Unlock() -} - -var ( - loggerMutex = new(sync.Mutex) - logger logInternal // A logger that can be set by consumers -) - -// logProvider is a logger interface compatible with both stdlib and some -// 3rd party loggers such as logrus. -type logProvider interface { +// logger is a logger interface compatible with both stdlib and some +// 3rd party loggers. +type logger interface { Output(int, string) error } -// logInternal represents the internal logging api we use. -type logInternal interface { +// ilogger represents the internal logging api we use. +type ilogger interface { + logger Print(...interface{}) Printf(string, ...interface{}) Println(...interface{}) - Output(int, string) error } -// ilogger implements the additional methods used by our internal logging. -type ilogger struct { - logProvider +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 ilogger) Println(v ...interface{}) { +func (t internalLog) Println(v ...interface{}) { t.Output(2, fmt.Sprintln(v...)) } // Printf replicates the behaviour of the standard logger. -func (t ilogger) Printf(format string, v ...interface{}) { +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 ilogger) Print(v ...interface{}) { +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/vendor/github.com/nlopes/slack/messages.go b/vendor/github.com/nlopes/slack/messages.go index 6551dd4f..37a26335 100644 --- a/vendor/github.com/nlopes/slack/messages.go +++ b/vendor/github.com/nlopes/slack/messages.go @@ -4,17 +4,19 @@ package slack type OutgoingMessage struct { ID int `json:"id"` // channel ID - Channel string `json:"channel,omitempty"` - Text string `json:"text,omitempty"` - Type string `json:"type,omitempty"` - ThreadTimestamp string `json:"thread_ts,omitempty"` - ThreadBroadcast bool `json:"reply_broadcast,omitempty"` + Channel string `json:"channel,omitempty"` + Text string `json:"text,omitempty"` + Type string `json:"type,omitempty"` + ThreadTimestamp string `json:"thread_ts,omitempty"` + ThreadBroadcast bool `json:"reply_broadcast,omitempty"` + IDs []string `json:"ids,omitempty"` } // Message is an auxiliary type to allow us to have a message containing sub messages type Message struct { Msg SubMessage *Msg `json:"message,omitempty"` + PreviousMessage *Msg `json:"previous_message,omitempty"` } // Msg contains information about a slack message @@ -91,8 +93,18 @@ type Msg struct { ResponseType string `json:"response_type,omitempty"` ReplaceOriginal bool `json:"replace_original"` DeleteOriginal bool `json:"delete_original"` + + // Block type Message + Blocks Blocks `json:"blocks,omitempty"` } +const ( + // ResponseTypeInChannel in channel response for slash commands. + ResponseTypeInChannel = "in_channel" + // ResponseTypeEphemeral ephemeral respone for slash commands. + ResponseTypeEphemeral = "ephemeral" +) + // Icon is used for bot messages type Icon struct { IconURL string `json:"icon_url,omitempty"` @@ -147,6 +159,15 @@ func (rtm *RTM) NewOutgoingMessage(text string, channelID string, options ...RTM return &msg } +// NewSubscribeUserPresence prepares an OutgoingMessage that the user can +// use to subscribe presence events for the specified users. +func (rtm *RTM) NewSubscribeUserPresence(ids []string) *OutgoingMessage { + return &OutgoingMessage{ + Type: "presence_sub", + IDs: ids, + } +} + // NewTypingMessage prepares an OutgoingMessage that the user can // use to send as a typing indicator. Use this function to properly set the // messageID. @@ -174,5 +195,4 @@ func RTMsgOptionBroadcast() RTMsgOption { return func(msg *OutgoingMessage) { msg.ThreadBroadcast = true } - } diff --git a/vendor/github.com/nlopes/slack/misc.go b/vendor/github.com/nlopes/slack/misc.go index 69103384..0dcee950 100644 --- a/vendor/github.com/nlopes/slack/misc.go +++ b/vendor/github.com/nlopes/slack/misc.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "io/ioutil" + "mime" "mime/multipart" "net/http" "net/http/httputil" @@ -19,6 +20,7 @@ import ( "time" ) +// SlackResponse handles parsing out errors from the web api. type SlackResponse struct { Ok bool `json:"ok"` Error string `json:"error"` @@ -47,64 +49,112 @@ type statusCodeError struct { } func (t statusCodeError) Error() string { - // TODO: this is a bad error string, should clean it up with a breaking changes - // merger. - return fmt.Sprintf("Slack server error: %s.", t.Status) + return fmt.Sprintf("slack server error: %s", t.Status) } func (t statusCodeError) HTTPStatusCode() int { return t.Code } +func (t statusCodeError) Retryable() bool { + if t.Code >= 500 || t.Code == http.StatusTooManyRequests { + return true + } + return false +} + +// RateLimitedError represents the rate limit respond from slack type RateLimitedError struct { RetryAfter time.Duration } func (e *RateLimitedError) Error() string { - return fmt.Sprintf("Slack rate limit exceeded, retry after %s", e.RetryAfter) + return fmt.Sprintf("slack rate limit exceeded, retry after %s", e.RetryAfter) } -func fileUploadReq(ctx context.Context, path, fieldname, filename string, values url.Values, r io.Reader) (*http.Request, error) { - body := &bytes.Buffer{} - wr := multipart.NewWriter(body) +func (e *RateLimitedError) Retryable() bool { + return true +} - ioWriter, err := wr.CreateFormFile(fieldname, filename) +func fileUploadReq(ctx context.Context, path string, values url.Values, r io.Reader) (*http.Request, error) { + req, err := http.NewRequest("POST", path, r) if err != nil { - wr.Close() return nil, err } - _, err = io.Copy(ioWriter, r) + + req = req.WithContext(ctx) + req.URL.RawQuery = (values).Encode() + return req, nil +} + +func downloadFile(client httpClient, token string, downloadURL string, writer io.Writer, d debug) error { + if downloadURL == "" { + return fmt.Errorf("received empty download URL") + } + + req, err := http.NewRequest("GET", downloadURL, &bytes.Buffer{}) if err != nil { - wr.Close() - return nil, err + return err } - // Close the multipart writer or the footer won't be written - wr.Close() - req, err := http.NewRequest("POST", path, body) - req = req.WithContext(ctx) + + var bearer = "Bearer " + token + req.Header.Add("Authorization", bearer) + req.WithContext(context.Background()) + + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + err = checkStatusCode(resp, d) if err != nil { + return err + } + + _, err = io.Copy(writer, resp.Body) + + return err +} + +func formReq(endpoint string, values url.Values) (req *http.Request, err error) { + if req, err = http.NewRequest("POST", endpoint, strings.NewReader(values.Encode())); err != nil { return nil, err } - req.Header.Add("Content-Type", wr.FormDataContentType()) - req.URL.RawQuery = (values).Encode() + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return req, nil +} + +func jsonReq(endpoint string, body interface{}) (req *http.Request, err error) { + buffer := bytes.NewBuffer([]byte{}) + if err = json.NewEncoder(buffer).Encode(body); err != nil { + return nil, err + } + + if req, err = http.NewRequest("POST", endpoint, buffer); err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") return req, nil } -func parseResponseBody(body io.ReadCloser, intf interface{}, debug bool) error { +func parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error { response, err := ioutil.ReadAll(body) if err != nil { return err } - // FIXME: will be api.Debugf - if debug { - logger.Printf("parseResponseBody: %s\n", string(response)) + if d.Debug() { + d.Debugln("parseResponseBody", string(response)) } return json.Unmarshal(response, intf) } -func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error { +func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname string, values url.Values, intf interface{}, d debug) error { fullpath, err := filepath.Abs(fpath) if err != nil { return err @@ -114,39 +164,58 @@ func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, p return err } defer file.Close() - return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, debug) + + return postWithMultipartResponse(ctx, client, method, filepath.Base(fpath), fieldname, values, file, intf, d) } -func postWithMultipartResponse(ctx context.Context, client HTTPRequester, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, debug bool) error { - req, err := fileUploadReq(ctx, SLACK_API+path, fieldname, name, values, r) +func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, d debug) error { + pipeReader, pipeWriter := io.Pipe() + wr := multipart.NewWriter(pipeWriter) + errc := make(chan error) + go func() { + defer pipeWriter.Close() + ioWriter, err := wr.CreateFormFile(fieldname, name) + if err != nil { + errc <- err + return + } + _, err = io.Copy(ioWriter, r) + if err != nil { + errc <- err + return + } + if err = wr.Close(); err != nil { + errc <- err + return + } + }() + req, err := fileUploadReq(ctx, path, values, pipeReader) if err != nil { return err } + req.Header.Add("Content-Type", wr.FormDataContentType()) req = req.WithContext(ctx) resp, err := client.Do(req) + if err != nil { return err } defer resp.Body.Close() - if resp.StatusCode == http.StatusTooManyRequests { - retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64) - if err != nil { - return err - } - return &RateLimitedError{time.Duration(retry) * time.Second} + err = checkStatusCode(resp, d) + if err != nil { + return err } - // Slack seems to send an HTML body along with 5xx error codes. Don't parse it. - if resp.StatusCode != http.StatusOK { - logResponse(resp, debug) - return statusCodeError{Code: resp.StatusCode, Status: resp.Status} + select { + case err = <-errc: + return err + default: + return newJSONParser(intf)(resp) } - - return parseResponseBody(resp.Body, intf, debug) } -func doPost(ctx context.Context, client HTTPRequester, req *http.Request, intf interface{}, debug bool) error { +func doPost(ctx context.Context, client httpClient, req *http.Request, parser responseParser, d debug) error { req = req.WithContext(ctx) resp, err := client.Do(req) if err != nil { @@ -154,25 +223,16 @@ func doPost(ctx context.Context, client HTTPRequester, req *http.Request, intf i } defer resp.Body.Close() - if resp.StatusCode == http.StatusTooManyRequests { - retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64) - if err != nil { - return err - } - return &RateLimitedError{time.Duration(retry) * time.Second} - } - - // Slack seems to send an HTML body along with 5xx error codes. Don't parse it. - if resp.StatusCode != http.StatusOK { - logResponse(resp, debug) - return statusCodeError{Code: resp.StatusCode, Status: resp.Status} + err = checkStatusCode(resp, d) + if err != nil { + return err } - return parseResponseBody(resp.Body, intf, debug) + return parser(resp) } // post JSON. -func postJSON(ctx context.Context, client HTTPRequester, endpoint, token string, json []byte, intf interface{}, debug bool) error { +func postJSON(ctx context.Context, client httpClient, endpoint, token string, json []byte, intf interface{}, d debug) error { reqBody := bytes.NewBuffer(json) req, err := http.NewRequest("POST", endpoint, reqBody) if err != nil { @@ -180,38 +240,44 @@ func postJSON(ctx context.Context, client HTTPRequester, endpoint, token string, } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - return doPost(ctx, client, req, intf, debug) + + return doPost(ctx, client, req, newJSONParser(intf), d) } // post a url encoded form. -func postForm(ctx context.Context, client HTTPRequester, endpoint string, values url.Values, intf interface{}, debug bool) error { +func postForm(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error { reqBody := strings.NewReader(values.Encode()) req, err := http.NewRequest("POST", endpoint, reqBody) if err != nil { return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - return doPost(ctx, client, req, intf, debug) + return doPost(ctx, client, req, newJSONParser(intf), d) } -// post to a slack web method. -func postSlackMethod(ctx context.Context, client HTTPRequester, path string, values url.Values, intf interface{}, debug bool) error { - return postForm(ctx, client, SLACK_API+path, values, intf, debug) +func getResource(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error { + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.URL.RawQuery = values.Encode() + + return doPost(ctx, client, req, newJSONParser(intf), d) } -func parseAdminResponse(ctx context.Context, client HTTPRequester, method string, teamName string, values url.Values, intf interface{}, debug bool) error { - endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix()) - return postForm(ctx, client, endpoint, values, intf, debug) +func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf interface{}, d debug) error { + endpoint := fmt.Sprintf(WEBAPIURLFormat, teamName, method, time.Now().Unix()) + return postForm(ctx, client, endpoint, values, intf, d) } -func logResponse(resp *http.Response, debug bool) error { - if debug { +func logResponse(resp *http.Response, d debug) error { + if d.Debug() { text, err := httputil.DumpResponse(resp, true) if err != nil { return err } - - logger.Print(string(text)) + d.Debugln(string(text)) } return nil @@ -225,12 +291,6 @@ func okJSONHandler(rw http.ResponseWriter, r *http.Request) { rw.Write(response) } -type errorString string - -func (t errorString) Error() string { - return string(t) -} - // timerReset safely reset a timer, see time.Timer.Reset for details. func timerReset(t *time.Timer, d time.Duration) { if !t.Stop() { @@ -238,3 +298,63 @@ func timerReset(t *time.Timer, d time.Duration) { } t.Reset(d) } + +func checkStatusCode(resp *http.Response, d debug) error { + if resp.StatusCode == http.StatusTooManyRequests { + retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64) + if err != nil { + return err + } + return &RateLimitedError{time.Duration(retry) * time.Second} + } + + // Slack seems to send an HTML body along with 5xx error codes. Don't parse it. + if resp.StatusCode != http.StatusOK { + logResponse(resp, d) + return statusCodeError{Code: resp.StatusCode, Status: resp.Status} + } + + return nil +} + +type responseParser func(*http.Response) error + +func newJSONParser(dst interface{}) responseParser { + return func(resp *http.Response) error { + return json.NewDecoder(resp.Body).Decode(dst) + } +} + +func newTextParser(dst interface{}) responseParser { + return func(resp *http.Response) error { + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + if !bytes.Equal(b, []byte("ok")) { + return errors.New(string(b)) + } + + return nil + } +} + +func newContentTypeParser(dst interface{}) responseParser { + return func(req *http.Response) (err error) { + var ( + ctype string + ) + + if ctype, _, err = mime.ParseMediaType(req.Header.Get("Content-Type")); err != nil { + return err + } + + switch ctype { + case "application/json": + return newJSONParser(dst)(req) + default: + return newTextParser(dst)(req) + } + } +} diff --git a/vendor/github.com/nlopes/slack/oauth.go b/vendor/github.com/nlopes/slack/oauth.go index 378af4a5..29d6dce9 100644 --- a/vendor/github.com/nlopes/slack/oauth.go +++ b/vendor/github.com/nlopes/slack/oauth.go @@ -2,10 +2,10 @@ package slack import ( "context" - "errors" "net/url" ) +// OAuthResponseIncomingWebhook ... type OAuthResponseIncomingWebhook struct { URL string `json:"url"` Channel string `json:"channel"` @@ -13,11 +13,13 @@ type OAuthResponseIncomingWebhook struct { ConfigurationURL string `json:"configuration_url"` } +// OAuthResponseBot ... type OAuthResponseBot struct { BotUserID string `json:"bot_user_id"` BotAccessToken string `json:"bot_access_token"` } +// OAuthResponse ... type OAuthResponse struct { AccessToken string `json:"access_token"` Scope string `json:"scope"` @@ -30,24 +32,24 @@ type OAuthResponse struct { } // GetOAuthToken retrieves an AccessToken -func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) { - return GetOAuthTokenContext(context.Background(), clientID, clientSecret, code, redirectURI, debug) +func GetOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) { + return GetOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI) } // GetOAuthTokenContext retrieves an AccessToken with a custom context -func GetOAuthTokenContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) { - response, err := GetOAuthResponseContext(ctx, clientID, clientSecret, code, redirectURI, debug) +func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) { + response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI) if err != nil { return "", "", err } return response.AccessToken, response.Scope, nil } -func GetOAuthResponse(clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) { - return GetOAuthResponseContext(context.Background(), clientID, clientSecret, code, redirectURI, debug) +func GetOAuthResponse(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) { + return GetOAuthResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI) } -func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) { +func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) { values := url.Values{ "client_id": {clientID}, "client_secret": {clientSecret}, @@ -55,12 +57,8 @@ func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code, "redirect_uri": {redirectURI}, } response := &OAuthResponse{} - err = postSlackMethod(ctx, customHTTPClient, "oauth.access", values, response, debug) - if err != nil { + if err = postForm(ctx, client, APIURL+"oauth.access", values, response, discard{}); err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + return response, response.Err() } diff --git a/vendor/github.com/nlopes/slack/pins.go b/vendor/github.com/nlopes/slack/pins.go index 34863f17..ef97c8df 100644 --- a/vendor/github.com/nlopes/slack/pins.go +++ b/vendor/github.com/nlopes/slack/pins.go @@ -34,7 +34,7 @@ func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemR } response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "pins.add", values, response, api.debug); err != nil { + if err := api.postMethod(ctx, "pins.add", values, response); err != nil { return err } @@ -63,7 +63,7 @@ func (api *Client) RemovePinContext(ctx context.Context, channel string, item It } response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "pins.remove", values, response, api.debug); err != nil { + if err := api.postMethod(ctx, "pins.remove", values, response); err != nil { return err } @@ -83,7 +83,7 @@ func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item, } response := &listPinsResponseFull{} - err := postSlackMethod(ctx, api.httpclient, "pins.list", values, response, api.debug) + err := api.postMethod(ctx, "pins.list", values, response) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/reactions.go b/vendor/github.com/nlopes/slack/reactions.go index 5eabde63..2a9bd42e 100644 --- a/vendor/github.com/nlopes/slack/reactions.go +++ b/vendor/github.com/nlopes/slack/reactions.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strconv" ) @@ -155,7 +154,7 @@ func (api *Client) AddReactionContext(ctx context.Context, name string, item Ite } response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "reactions.add", values, response, api.debug); err != nil { + if err := api.postMethod(ctx, "reactions.add", values, response); err != nil { return err } @@ -189,7 +188,7 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item } response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "reactions.remove", values, response, api.debug); err != nil { + if err := api.postMethod(ctx, "reactions.remove", values, response); err != nil { return err } @@ -223,12 +222,14 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params } response := &getReactionsResponseFull{} - if err := postSlackMethod(ctx, api.httpclient, "reactions.get", values, response, api.debug); err != nil { + if err := api.postMethod(ctx, "reactions.get", values, response); err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) + + if err := response.Err(); err != nil { + return nil, err } + return response.extractReactions(), nil } @@ -256,12 +257,14 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction } response := &listReactionsResponseFull{} - err := postSlackMethod(ctx, api.httpclient, "reactions.list", values, response, api.debug) + err := api.postMethod(ctx, "reactions.list", values, response) if err != nil { return nil, nil, err } - if !response.Ok { - return nil, nil, errors.New(response.Error) + + if err := response.Err(); err != nil { + return nil, nil, err } + return response.extractReactedItems(), &response.Paging, nil } diff --git a/vendor/github.com/nlopes/slack/reminders.go b/vendor/github.com/nlopes/slack/reminders.go new file mode 100644 index 00000000..9b905387 --- /dev/null +++ b/vendor/github.com/nlopes/slack/reminders.go @@ -0,0 +1,75 @@ +package slack + +import ( + "context" + "net/url" + "time" +) + +type Reminder struct { + ID string `json:"id"` + Creator string `json:"creator"` + User string `json:"user"` + Text string `json:"text"` + Recurring bool `json:"recurring"` + Time time.Time `json:"time"` + CompleteTS int `json:"complete_ts"` +} + +type reminderResp struct { + SlackResponse + Reminder Reminder `json:"reminder"` +} + +func (api *Client) doReminder(ctx context.Context, path string, values url.Values) (*Reminder, error) { + response := &reminderResp{} + if err := api.postMethod(ctx, path, values, response); err != nil { + return nil, err + } + return &response.Reminder, response.Err() +} + +// AddChannelReminder adds a reminder for a channel. +// +// See https://api.slack.com/methods/reminders.add (NOTE: the ability to set +// reminders on a channel is currently undocumented but has been tested to +// work) +func (api *Client) AddChannelReminder(channelID, text, time string) (*Reminder, error) { + values := url.Values{ + "token": {api.token}, + "text": {text}, + "time": {time}, + "channel": {channelID}, + } + return api.doReminder(context.Background(), "reminders.add", values) +} + +// AddUserReminder adds a reminder for a user. +// +// See https://api.slack.com/methods/reminders.add (NOTE: the ability to set +// reminders on a channel is currently undocumented but has been tested to +// work) +func (api *Client) AddUserReminder(userID, text, time string) (*Reminder, error) { + values := url.Values{ + "token": {api.token}, + "text": {text}, + "time": {time}, + "user": {userID}, + } + return api.doReminder(context.Background(), "reminders.add", values) +} + +// DeleteReminder deletes an existing reminder. +// +// See https://api.slack.com/methods/reminders.delete +func (api *Client) DeleteReminder(id string) error { + values := url.Values{ + "token": {api.token}, + "reminder": {id}, + } + response := &SlackResponse{} + if err := api.postMethod(context.Background(), "reminders.delete", values, response); err != nil { + return err + } + return response.Err() +} diff --git a/vendor/github.com/nlopes/slack/rtm.go b/vendor/github.com/nlopes/slack/rtm.go index 41a136eb..09cb51c3 100644 --- a/vendor/github.com/nlopes/slack/rtm.go +++ b/vendor/github.com/nlopes/slack/rtm.go @@ -38,7 +38,7 @@ func (api *Client) StartRTM() (info *Info, websocketURL string, err error) { // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) { response := &infoResponseFull{} - err = postSlackMethod(ctx, api.httpclient, "rtm.start", url.Values{"token": {api.token}}, response, api.debug) + err = api.postMethod(ctx, "rtm.start", url.Values{"token": {api.token}}, response) if err != nil { return nil, "", err } @@ -63,7 +63,7 @@ func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) { // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) { response := &infoResponseFull{} - err = postSlackMethod(ctx, api.httpclient, "rtm.connect", url.Values{"token": {api.token}}, response, api.debug) + err = api.postMethod(ctx, "rtm.connect", url.Values{"token": {api.token}}, response) if err != nil { api.Debugf("Failed to connect to RTM: %s", err) return nil, "", err @@ -100,6 +100,13 @@ func RTMOptionPingInterval(d time.Duration) RTMOption { } } +// RTMOptionConnParams installs parameters to embed into the connection URL. +func RTMOptionConnParams(connParams url.Values) RTMOption { + return func(rtm *RTM) { + rtm.connParams = connParams + } +} + // NewRTM returns a RTM, which provides a fully managed connection to // Slack's websocket-based Real-Time Messaging protocol. func (api *Client) NewRTM(options ...RTMOption) *RTM { @@ -109,10 +116,9 @@ func (api *Client) NewRTM(options ...RTMOption) *RTM { outgoingMessages: make(chan OutgoingMessage, 20), pingInterval: defaultPingInterval, pingDeadman: time.NewTimer(deadmanDuration(defaultPingInterval)), - isConnected: false, - wasIntentional: true, killChannel: make(chan bool), - disconnected: make(chan struct{}, 1), + disconnected: make(chan struct{}), + disconnectedm: &sync.Once{}, forcePing: make(chan bool), rawEvents: make(chan json.RawMessage), idGen: NewSafeID(1), @@ -125,14 +131,3 @@ func (api *Client) NewRTM(options ...RTMOption) *RTM { return result } - -// NewRTMWithOptions Deprecated just use NewRTM(RTMOptionsUseStart(true)) -// returns a RTM, which provides a fully managed connection to -// Slack's websocket-based Real-Time Messaging protocol. -// This also allows to configure various options available for RTM API. -func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM { - if options != nil { - return api.NewRTM(RTMOptionUseStart(options.UseRTMStart)) - } - return api.NewRTM() -} diff --git a/vendor/github.com/nlopes/slack/search.go b/vendor/github.com/nlopes/slack/search.go index e858952f..67e3b1d1 100644 --- a/vendor/github.com/nlopes/slack/search.go +++ b/vendor/github.com/nlopes/slack/search.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strconv" ) @@ -42,6 +41,7 @@ type SearchMessage struct { User string `json:"user"` Username string `json:"username"` Timestamp string `json:"ts"` + Blocks Blocks `json:"blocks,omitempty"` Text string `json:"text"` Permalink string `json:"permalink"` Attachments []Attachment `json:"attachments"` @@ -104,14 +104,12 @@ func (api *Client) _search(ctx context.Context, path, query string, params Searc } response = &searchResponseFull{} - err := postSlackMethod(ctx, api.httpclient, path, values, response, api.debug) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } diff --git a/vendor/github.com/nlopes/slack/security.go b/vendor/github.com/nlopes/slack/security.go new file mode 100644 index 00000000..dbe8fb2d --- /dev/null +++ b/vendor/github.com/nlopes/slack/security.go @@ -0,0 +1,100 @@ +package slack + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" + "hash" + "net/http" + "strconv" + "strings" + "time" +) + +// Signature headers +const ( + hSignature = "X-Slack-Signature" + hTimestamp = "X-Slack-Request-Timestamp" +) + +// SecretsVerifier contains the information needed to verify that the request comes from Slack +type SecretsVerifier struct { + signature []byte + hmac hash.Hash +} + +func unsafeSignatureVerifier(header http.Header, secret string) (_ SecretsVerifier, err error) { + var ( + bsignature []byte + ) + + signature := header.Get(hSignature) + stimestamp := header.Get(hTimestamp) + + if signature == "" || stimestamp == "" { + return SecretsVerifier{}, ErrMissingHeaders + } + + if bsignature, err = hex.DecodeString(strings.TrimPrefix(signature, "v0=")); err != nil { + return SecretsVerifier{}, err + } + + hash := hmac.New(sha256.New, []byte(secret)) + if _, err = hash.Write([]byte(fmt.Sprintf("v0:%s:", stimestamp))); err != nil { + return SecretsVerifier{}, err + } + + return SecretsVerifier{ + signature: bsignature, + hmac: hash, + }, nil +} + +// NewSecretsVerifier returns a SecretsVerifier object in exchange for an http.Header object and signing secret +func NewSecretsVerifier(header http.Header, secret string) (sv SecretsVerifier, err error) { + var ( + timestamp int64 + ) + + stimestamp := header.Get(hTimestamp) + + if sv, err = unsafeSignatureVerifier(header, secret); err != nil { + return SecretsVerifier{}, err + } + + if timestamp, err = strconv.ParseInt(stimestamp, 10, 64); err != nil { + return SecretsVerifier{}, err + } + + diff := absDuration(time.Since(time.Unix(timestamp, 0))) + if diff > 5*time.Minute { + return SecretsVerifier{}, ErrExpiredTimestamp + } + + return sv, err +} + +func (v *SecretsVerifier) Write(body []byte) (n int, err error) { + return v.hmac.Write(body) +} + +// Ensure compares the signature sent from Slack with the actual computed hash to judge validity +func (v SecretsVerifier) Ensure() error { + computed := v.hmac.Sum(nil) + // use hmac.Equal prevent leaking timing information. + if hmac.Equal(computed, v.signature) { + return nil + } + + return fmt.Errorf("Expected signing signature: %s, but computed: %s", hex.EncodeToString(v.signature), hex.EncodeToString(computed)) +} + +func abs64(n int64) int64 { + y := n >> 63 + return (n ^ y) - y +} + +func absDuration(n time.Duration) time.Duration { + return time.Duration(abs64(int64(n))) +} diff --git a/vendor/github.com/nlopes/slack/slack.go b/vendor/github.com/nlopes/slack/slack.go index 6d1e7de9..94230526 100644 --- a/vendor/github.com/nlopes/slack/slack.go +++ b/vendor/github.com/nlopes/slack/slack.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "fmt" "log" "net/http" @@ -10,31 +9,18 @@ import ( "os" ) -// Added as a var so that we can change this for testing purposes -var SLACK_API string = "https://slack.com/api/" -var SLACK_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s" - -// HTTPClient sets a custom http.Client -// deprecated: in favor of SetHTTPClient() -var HTTPClient = &http.Client{} - -var customHTTPClient HTTPRequester = HTTPClient +const ( + // APIURL of the slack api. + APIURL = "https://slack.com/api/" + // WEBAPIURLFormat ... + WEBAPIURLFormat = "https://%s.slack.com/api/users.admin.%s?t=%d" +) -// HTTPRequester defines the minimal interface needed for an http.Client to be implemented. -// -// Use it in conjunction with the SetHTTPClient function to allow for other capabilities -// like a tracing http.Client -type HTTPRequester interface { +// httpClient defines the minimal interface needed for an http.Client to be implemented. +type httpClient interface { Do(*http.Request) (*http.Response, error) } -// SetHTTPClient allows you to specify a custom http.Client -// Use this instead of the package level HTTPClient variable if you want to use a custom client like the -// Stackdriver Trace HTTPClient https://godoc.org/cloud.google.com/go/trace#HTTPClient -func SetHTTPClient(client HTTPRequester) { - customHTTPClient = client -} - // ResponseMetadata holds pagination metadata type ResponseMetadata struct { Cursor string `json:"next_cursor"` @@ -48,12 +34,15 @@ func (t *ResponseMetadata) initialize() *ResponseMetadata { return &ResponseMetadata{} } +// AuthTestResponse ... type AuthTestResponse struct { URL string `json:"url"` Team string `json:"team"` User string `json:"user"` TeamID string `json:"team_id"` UserID string `json:"user_id"` + // EnterpriseID is only returned when an enterprise id present + EnterpriseID string `json:"enterprise_id,omitempty"` } type authTestResponseFull struct { @@ -61,28 +50,53 @@ type authTestResponseFull struct { AuthTestResponse } +// Client for the slack api. +type ParamOption func(*url.Values) + type Client struct { token string - info Info + endpoint string debug bool - httpclient HTTPRequester + log ilogger + httpclient httpClient } // Option defines an option for a Client type Option func(*Client) // OptionHTTPClient - provide a custom http client to the slack client. -func OptionHTTPClient(c HTTPRequester) func(*Client) { - return func(s *Client) { - s.httpclient = c +func OptionHTTPClient(client httpClient) func(*Client) { + return func(c *Client) { + c.httpclient = client + } +} + +// OptionDebug enable debugging for the client +func OptionDebug(b bool) func(*Client) { + return func(c *Client) { + c.debug = b + } +} + +// OptionLog set logging for client. +func OptionLog(l logger) func(*Client) { + return func(c *Client) { + c.log = internalLog{logger: l} } } +// OptionAPIURL set the url for the client. only useful for testing. +func OptionAPIURL(u string) func(*Client) { + return func(c *Client) { c.endpoint = u } +} + // New builds a slack client from the provided token and options. func New(token string, options ...Option) *Client { s := &Client{ token: token, - httpclient: customHTTPClient, + endpoint: APIURL, + httpclient: &http.Client{}, + log: log.New(os.Stderr, "nlopes/slack", log.LstdFlags|log.Lshortfile), } for _, opt := range options { @@ -98,43 +112,42 @@ func (api *Client) AuthTest() (response *AuthTestResponse, error error) { } // AuthTestContext tests if the user is able to do authenticated requests or not with a custom context -func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, error error) { +func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, err error) { api.Debugf("Challenging auth...") responseFull := &authTestResponseFull{} - err := postSlackMethod(ctx, api.httpclient, "auth.test", url.Values{"token": {api.token}}, responseFull, api.debug) + err = api.postMethod(ctx, "auth.test", url.Values{"token": {api.token}}, responseFull) if err != nil { - api.Debugf("failed to test for auth: %s", err) return nil, err } - if !responseFull.Ok { - api.Debugf("auth response was not Ok: %s", responseFull.Error) - return nil, errors.New(responseFull.Error) - } - api.Debugf("Auth challenge was successful with response %+v", responseFull.AuthTestResponse) - return &responseFull.AuthTestResponse, nil -} - -// SetDebug switches the api into debug mode -// When in debug mode, it logs various info about what its doing -// If you ever use this in production, don't call SetDebug(true) -func (api *Client) SetDebug(debug bool) { - api.debug = debug - if debug && logger == nil { - SetLogger(log.New(os.Stdout, "nlopes/slack", log.LstdFlags|log.Lshortfile)) - } + return &responseFull.AuthTestResponse, responseFull.Err() } // Debugf print a formatted debug line. func (api *Client) Debugf(format string, v ...interface{}) { if api.debug { - logger.Output(2, fmt.Sprintf(format, v...)) + api.log.Output(2, fmt.Sprintf(format, v...)) } } // Debugln print a debug line. func (api *Client) Debugln(v ...interface{}) { if api.debug { - logger.Output(2, fmt.Sprintln(v...)) + api.log.Output(2, fmt.Sprintln(v...)) } } + +// Debug returns if debug is enabled. +func (api *Client) Debug() bool { + return api.debug +} + +// post to a slack web method. +func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf interface{}) error { + return postForm(ctx, api.httpclient, api.endpoint+path, values, intf, api) +} + +// get a slack web method. +func (api *Client) getMethod(ctx context.Context, path string, values url.Values, intf interface{}) error { + return getResource(ctx, api.httpclient, api.endpoint+path, values, intf, api) +} diff --git a/vendor/github.com/nlopes/slack/slackutilsx/slackutilsx.go b/vendor/github.com/nlopes/slack/slackutilsx/slackutilsx.go index ccf5372b..1f7b2b8c 100644 --- a/vendor/github.com/nlopes/slack/slackutilsx/slackutilsx.go +++ b/vendor/github.com/nlopes/slack/slackutilsx/slackutilsx.go @@ -55,3 +55,8 @@ func EscapeMessage(message string) string { replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") return replacer.Replace(message) } + +// Retryable errors return true. +type Retryable interface { + Retryable() bool +} diff --git a/vendor/github.com/nlopes/slack/stars.go b/vendor/github.com/nlopes/slack/stars.go index c1e2f6cb..e84d0447 100644 --- a/vendor/github.com/nlopes/slack/stars.go +++ b/vendor/github.com/nlopes/slack/stars.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strconv" ) @@ -58,7 +57,7 @@ func (api *Client) AddStarContext(ctx context.Context, channel string, item Item } response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "stars.add", values, response, api.debug); err != nil { + if err := api.postMethod(ctx, "stars.add", values, response); err != nil { return err } @@ -87,7 +86,7 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I } response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "stars.remove", values, response, api.debug); err != nil { + if err := api.postMethod(ctx, "stars.remove", values, response); err != nil { return err } @@ -115,13 +114,15 @@ func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) } response := &listResponseFull{} - err := postSlackMethod(ctx, api.httpclient, "stars.list", values, response, api.debug) + err := api.postMethod(ctx, "stars.list", values, response) if err != nil { return nil, nil, err } - if !response.Ok { - return nil, nil, errors.New(response.Error) + + if err := response.Err(); err != nil { + return nil, nil, err } + return response.Items, &response.Paging, nil } diff --git a/vendor/github.com/nlopes/slack/team.go b/vendor/github.com/nlopes/slack/team.go index b6e341eb..029e2b5b 100644 --- a/vendor/github.com/nlopes/slack/team.go +++ b/vendor/github.com/nlopes/slack/team.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strconv" ) @@ -67,44 +66,33 @@ func NewAccessLogParameters() AccessLogParameters { } } -func teamRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*TeamResponse, error) { +func (api *Client) teamRequest(ctx context.Context, path string, values url.Values) (*TeamResponse, error) { response := &TeamResponse{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - - return response, nil + return response, response.Err() } -func billableInfoRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (map[string]BillingActive, error) { +func (api *Client) billableInfoRequest(ctx context.Context, path string, values url.Values) (map[string]BillingActive, error) { response := &BillableInfoResponse{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - - return response.BillableInfo, nil + return response.BillableInfo, response.Err() } -func accessLogsRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*LoginResponse, error) { +func (api *Client) accessLogsRequest(ctx context.Context, path string, values url.Values) (*LoginResponse, error) { response := &LoginResponse{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + return response, response.Err() } // GetTeamInfo gets the Team Information of the user @@ -118,7 +106,7 @@ func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) { "token": {api.token}, } - response, err := teamRequest(ctx, api.httpclient, "team.info", values, api.debug) + response, err := api.teamRequest(ctx, "team.info", values) if err != nil { return nil, err } @@ -142,24 +130,26 @@ func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogPar values.Add("page", strconv.Itoa(params.Page)) } - response, err := accessLogsRequest(ctx, api.httpclient, "team.accessLogs", values, api.debug) + response, err := api.accessLogsRequest(ctx, "team.accessLogs", values) if err != nil { return nil, nil, err } return response.Logins, &response.Paging, nil } +// GetBillableInfo ... func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error) { return api.GetBillableInfoContext(context.Background(), user) } +// GetBillableInfoContext ... func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map[string]BillingActive, error) { values := url.Values{ "token": {api.token}, "user": {user}, } - return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug) + return api.billableInfoRequest(ctx, "team.billableInfo", values) } // GetBillableInfoForTeam returns the billing_active status of all users on the team. @@ -173,5 +163,5 @@ func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[strin "token": {api.token}, } - return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug) + return api.billableInfoRequest(ctx, "team.billableInfo", values) } diff --git a/vendor/github.com/nlopes/slack/usergroups.go b/vendor/github.com/nlopes/slack/usergroups.go index cc9bc4ca..f3206591 100644 --- a/vendor/github.com/nlopes/slack/usergroups.go +++ b/vendor/github.com/nlopes/slack/usergroups.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strings" ) @@ -41,16 +40,14 @@ type userGroupResponseFull struct { SlackResponse } -func userGroupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userGroupResponseFull, error) { +func (api *Client) userGroupRequest(ctx context.Context, path string, values url.Values) (*userGroupResponseFull, error) { response := &userGroupResponseFull{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } // CreateUserGroup creates a new user group @@ -77,7 +74,7 @@ func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGro values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")} } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.create", values, api.debug) + response, err := api.userGroupRequest(ctx, "usergroups.create", values) if err != nil { return UserGroup{}, err } @@ -96,7 +93,7 @@ func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string "usergroup": {userGroup}, } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.disable", values, api.debug) + response, err := api.userGroupRequest(ctx, "usergroups.disable", values) if err != nil { return UserGroup{}, err } @@ -115,7 +112,7 @@ func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) "usergroup": {userGroup}, } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.enable", values, api.debug) + response, err := api.userGroupRequest(ctx, "usergroups.enable", values) if err != nil { return UserGroup{}, err } @@ -179,7 +176,7 @@ func (api *Client) GetUserGroupsContext(ctx context.Context, options ...GetUserG values.Add("include_users", "true") } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.list", values, api.debug) + response, err := api.userGroupRequest(ctx, "usergroups.list", values) if err != nil { return nil, err } @@ -209,8 +206,12 @@ func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGro if userGroup.Description != "" { values["description"] = []string{userGroup.Description} } + + if len(userGroup.Prefs.Channels) > 0 { + values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")} + } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.update", values, api.debug) + response, err := api.userGroupRequest(ctx, "usergroups.update", values) if err != nil { return UserGroup{}, err } @@ -229,7 +230,7 @@ func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup str "usergroup": {userGroup}, } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.list", values, api.debug) + response, err := api.userGroupRequest(ctx, "usergroups.users.list", values) if err != nil { return []string{}, err } @@ -249,7 +250,7 @@ func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup "users": {members}, } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.update", values, api.debug) + response, err := api.userGroupRequest(ctx, "usergroups.users.update", values) if err != nil { return UserGroup{}, err } diff --git a/vendor/github.com/nlopes/slack/users.go b/vendor/github.com/nlopes/slack/users.go index 0dd20db5..4da8e4ce 100644 --- a/vendor/github.com/nlopes/slack/users.go +++ b/vendor/github.com/nlopes/slack/users.go @@ -3,16 +3,15 @@ package slack import ( "context" "encoding/json" - "errors" "net/url" "strconv" + "time" ) const ( DEFAULT_USER_PHOTO_CROP_X = -1 DEFAULT_USER_PHOTO_CROP_Y = -1 DEFAULT_USER_PHOTO_CROP_W = -1 - errPaginationComplete = errorString("pagination complete") ) // UserProfile contains all the information details of a given user @@ -37,6 +36,7 @@ type UserProfile struct { ApiAppID string `json:"api_app_id,omitempty"` StatusText string `json:"status_text,omitempty"` StatusEmoji string `json:"status_emoji,omitempty"` + StatusExpiration int `json:"status_expiration"` Team string `json:"team"` Fields UserProfileCustomFields `json:"fields"` } @@ -100,28 +100,31 @@ type UserProfileCustomField struct { // User contains all the information of a user type User struct { - ID string `json:"id"` - TeamID string `json:"team_id"` - Name string `json:"name"` - Deleted bool `json:"deleted"` - Color string `json:"color"` - RealName string `json:"real_name"` - TZ string `json:"tz,omitempty"` - TZLabel string `json:"tz_label"` - TZOffset int `json:"tz_offset"` - Profile UserProfile `json:"profile"` - IsBot bool `json:"is_bot"` - IsAdmin bool `json:"is_admin"` - IsOwner bool `json:"is_owner"` - IsPrimaryOwner bool `json:"is_primary_owner"` - IsRestricted bool `json:"is_restricted"` - IsUltraRestricted bool `json:"is_ultra_restricted"` - IsStranger bool `json:"is_stranger"` - IsAppUser bool `json:"is_app_user"` - Has2FA bool `json:"has_2fa"` - HasFiles bool `json:"has_files"` - Presence string `json:"presence"` - Locale string `json:"locale"` + ID string `json:"id"` + TeamID string `json:"team_id"` + Name string `json:"name"` + Deleted bool `json:"deleted"` + Color string `json:"color"` + RealName string `json:"real_name"` + TZ string `json:"tz,omitempty"` + TZLabel string `json:"tz_label"` + TZOffset int `json:"tz_offset"` + Profile UserProfile `json:"profile"` + IsBot bool `json:"is_bot"` + IsAdmin bool `json:"is_admin"` + IsOwner bool `json:"is_owner"` + IsPrimaryOwner bool `json:"is_primary_owner"` + IsRestricted bool `json:"is_restricted"` + IsUltraRestricted bool `json:"is_ultra_restricted"` + IsStranger bool `json:"is_stranger"` + IsAppUser bool `json:"is_app_user"` + IsInvitedUser bool `json:"is_invited_user"` + Has2FA bool `json:"has_2fa"` + HasFiles bool `json:"has_files"` + Presence string `json:"presence"` + Locale string `json:"locale"` + Updated JSONTime `json:"updated"` + Enterprise EnterpriseUser `json:"enterprise_user,omitempty"` } // UserPresence contains details about a user online status @@ -152,6 +155,17 @@ type UserIdentity struct { Image512 string `json:"image_512"` } +// EnterpriseUser is present when a user is part of Slack Enterprise Grid +// https://api.slack.com/types/user#enterprise_grid_user_objects +type EnterpriseUser struct { + ID string `json:"id"` + EnterpriseID string `json:"enterprise_id"` + EnterpriseName string `json:"enterprise_name"` + IsAdmin bool `json:"is_admin"` + IsOwner bool `json:"is_owner"` + Teams []string `json:"teams"` +} + type TeamIdentity struct { ID string `json:"id"` Name string `json:"name"` @@ -189,16 +203,14 @@ func NewUserSetPhotoParams() UserSetPhotoParams { } } -func userRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userResponseFull, error) { +func (api *Client) userRequest(ctx context.Context, path string, values url.Values) (*userResponseFull, error) { response := &userResponseFull{} - err := postForm(ctx, client, SLACK_API+path, values, response, debug) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } // GetUserPresence will retrieve the current presence status of given user. @@ -213,7 +225,7 @@ func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*Us "user": {user}, } - response, err := userRequest(ctx, api.httpclient, "users.getPresence", values, api.debug) + response, err := api.userRequest(ctx, "users.getPresence", values) if err != nil { return nil, err } @@ -228,11 +240,12 @@ func (api *Client) GetUserInfo(user string) (*User, error) { // GetUserInfoContext will retrieve the complete user information with a custom context func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) { values := url.Values{ - "token": {api.token}, - "user": {user}, + "token": {api.token}, + "user": {user}, + "include_locale": {strconv.FormatBool(true)}, } - response, err := userRequest(ctx, api.httpclient, "users.info", values, api.debug) + response, err := api.userRequest(ctx, "users.info", values) if err != nil { return nil, err } @@ -304,13 +317,14 @@ func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) t.previousResp = t.previousResp.initialize() values := url.Values{ - "limit": {strconv.Itoa(t.limit)}, - "presence": {strconv.FormatBool(t.presence)}, - "token": {t.c.token}, - "cursor": {t.previousResp.Cursor}, + "limit": {strconv.Itoa(t.limit)}, + "presence": {strconv.FormatBool(t.presence)}, + "token": {t.c.token}, + "cursor": {t.previousResp.Cursor}, + "include_locale": {strconv.FormatBool(true)}, } - if resp, err = userRequest(ctx, t.c.httpclient, "users.list", values, t.c.debug); err != nil { + if resp, err = t.c.userRequest(ctx, "users.list", values); err != nil { return t, err } @@ -333,12 +347,19 @@ func (api *Client) GetUsers() ([]User, error) { // GetUsersContext returns the list of users (with their detailed information) with a custom context func (api *Client) GetUsersContext(ctx context.Context) (results []User, err error) { - var ( - p UserPagination - ) - - for p = api.GetUsersPaginated(); !p.Done(err); p, err = p.Next(ctx) { - results = append(results, p.Users...) + p := api.GetUsersPaginated() + for err == nil { + p, err = p.Next(ctx) + if err == nil { + results = append(results, p.Users...) + } 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) @@ -355,7 +376,7 @@ func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*Us "token": {api.token}, "email": {email}, } - response, err := userRequest(ctx, api.httpclient, "users.lookupByEmail", values, api.debug) + response, err := api.userRequest(ctx, "users.lookupByEmail", values) if err != nil { return nil, err } @@ -373,7 +394,7 @@ func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) { "token": {api.token}, } - _, err = userRequest(ctx, api.httpclient, "users.setActive", values, api.debug) + _, err = api.userRequest(ctx, "users.setActive", values) return err } @@ -389,7 +410,7 @@ func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) "presence": {presence}, } - _, err := userRequest(ctx, api.httpclient, "users.setPresence", values, api.debug) + _, err := api.userRequest(ctx, "users.setPresence", values) return err } @@ -399,19 +420,21 @@ func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) { } // GetUserIdentityContext will retrieve user info available per identity scopes with a custom context -func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityResponse, error) { +func (api *Client) GetUserIdentityContext(ctx context.Context) (response *UserIdentityResponse, err error) { values := url.Values{ "token": {api.token}, } - response := &UserIdentityResponse{} + response = &UserIdentityResponse{} - err := postForm(ctx, api.httpclient, SLACK_API+"users.identity", values, response, api.debug) + err = api.postMethod(ctx, "users.identity", values, response) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) + + if err := response.Err(); err != nil { + return nil, err } + return response, nil } @@ -421,7 +444,7 @@ func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error { } // SetUserPhotoContext changes the currently authenticated user's profile image using a custom context -func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) error { +func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) (err error) { response := &SlackResponse{} values := url.Values{ "token": {api.token}, @@ -436,7 +459,7 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params values.Add("crop_w", strconv.Itoa(params.CropW)) } - err := postLocalWithMultipartResponse(ctx, api.httpclient, "users.setPhoto", image, "image", values, response, api.debug) + err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", values, response, api) if err != nil { return err } @@ -450,13 +473,13 @@ func (api *Client) DeleteUserPhoto() error { } // DeleteUserPhotoContext deletes the current authenticated user's profile image with a custom context -func (api *Client) DeleteUserPhotoContext(ctx context.Context) error { +func (api *Client) DeleteUserPhotoContext(ctx context.Context) (err error) { response := &SlackResponse{} values := url.Values{ "token": {api.token}, } - err := postForm(ctx, api.httpclient, SLACK_API+"users.deletePhoto", values, response, api.debug) + err = api.postMethod(ctx, "users.deletePhoto", values, response) if err != nil { return err } @@ -467,15 +490,30 @@ func (api *Client) DeleteUserPhotoContext(ctx context.Context) error { // SetUserCustomStatus will set a custom status and emoji for the currently // authenticated user. If statusEmoji is "" and statusText is not, the Slack API // will automatically set it to ":speech_balloon:". Otherwise, if both are "" -// the Slack API will unset the custom status/emoji. -func (api *Client) SetUserCustomStatus(statusText, statusEmoji string) error { - return api.SetUserCustomStatusContext(context.Background(), statusText, statusEmoji) +// the Slack API will unset the custom status/emoji. If statusExpiration is set to 0 +// the status will not expire. +func (api *Client) SetUserCustomStatus(statusText, statusEmoji string, statusExpiration int64) error { + return api.SetUserCustomStatusContextWithUser(context.Background(), "", statusText, statusEmoji, statusExpiration) } // SetUserCustomStatusContext will set a custom status and emoji for the currently authenticated user with a custom context // // For more information see SetUserCustomStatus -func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, statusEmoji string) error { +func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, statusEmoji string, statusExpiration int64) error { + return api.SetUserCustomStatusContextWithUser(context.Background(), "", statusText, statusEmoji, statusExpiration) +} + +// SetUserCustomStatusWithUser will set a custom status and emoji for the provided user. +// +// For more information see SetUserCustomStatus +func (api *Client) SetUserCustomStatusWithUser(user, statusText, statusEmoji string, statusExpiration int64) error { + return api.SetUserCustomStatusContextWithUser(context.Background(), user, statusText, statusEmoji, statusExpiration) +} + +// SetUserCustomStatusContextWithUser will set a custom status and emoji for the provided user with a custom context +// +// For more information see SetUserCustomStatus +func (api *Client) SetUserCustomStatusContextWithUser(ctx context.Context, user, statusText, statusEmoji string, statusExpiration int64) error { // XXX(theckman): this anonymous struct is for making requests to the Slack // API for setting and unsetting a User's Custom Status/Emoji. To change // these values we must provide a JSON document as the profile POST field. @@ -488,11 +526,13 @@ func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, s // - https://api.slack.com/docs/presence-and-status#custom_status profile, err := json.Marshal( &struct { - StatusText string `json:"status_text"` - StatusEmoji string `json:"status_emoji"` + StatusText string `json:"status_text"` + StatusEmoji string `json:"status_emoji"` + StatusExpiration int64 `json:"status_expiration"` }{ - StatusText: statusText, - StatusEmoji: statusEmoji, + StatusText: statusText, + StatusEmoji: statusEmoji, + StatusExpiration: statusExpiration, }, ) @@ -501,20 +541,17 @@ func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, s } values := url.Values{ + "user": {user}, "token": {api.token}, "profile": {string(profile)}, } response := &userResponseFull{} - if err = postForm(ctx, api.httpclient, SLACK_API+"users.profile.set", values, response, api.debug); err != nil { + if err = api.postMethod(ctx, "users.profile.set", values, response); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - - return nil + return response.Err() } // UnsetUserCustomStatus removes the custom status message for the currently @@ -526,7 +563,7 @@ func (api *Client) UnsetUserCustomStatus() error { // UnsetUserCustomStatusContext removes the custom status message for the currently authenticated user // with a custom context. This is a convenience method that wraps (*Client).SetUserCustomStatus(). func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error { - return api.SetUserCustomStatusContext(ctx, "", "") + return api.SetUserCustomStatusContext(ctx, "", "", 0) } // GetUserProfile retrieves a user's profile information. @@ -547,12 +584,14 @@ func (api *Client) GetUserProfileContext(ctx context.Context, userID string, inc } resp := &getUserProfileResponse{} - err := postSlackMethod(ctx, api.httpclient, "users.profile.get", values, &resp, api.debug) + err := api.postMethod(ctx, "users.profile.get", values, &resp) if err != nil { return nil, err } - if !resp.Ok { - return nil, errors.New(resp.Error) + + if err := resp.Err(); err != nil { + return nil, err } + return resp.Profile, nil } diff --git a/vendor/github.com/nlopes/slack/webhooks.go b/vendor/github.com/nlopes/slack/webhooks.go index 3ea69ffe..14e1b8dd 100644 --- a/vendor/github.com/nlopes/slack/webhooks.go +++ b/vendor/github.com/nlopes/slack/webhooks.go @@ -9,26 +9,32 @@ import ( ) type WebhookMessage struct { - Text string `json:"text,omitempty"` - Attachments []Attachment `json:"attachments,omitempty"` + Username string `json:"username,omitempty"` + IconEmoji string `json:"icon_emoji,omitempty"` + IconURL string `json:"icon_url,omitempty"` + Channel string `json:"channel,omitempty"` + ThreadTimestamp string `json:"thread_ts,omitempty"` + Text string `json:"text,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` + Parse string `json:"parse,omitempty"` } func PostWebhook(url string, msg *WebhookMessage) error { + return PostWebhookCustomHTTP(url, http.DefaultClient, msg) +} + +func PostWebhookCustomHTTP(url string, httpClient *http.Client, msg *WebhookMessage) error { raw, err := json.Marshal(msg) if err != nil { return errors.Wrap(err, "marshal failed") } - response, err := http.Post(url, "application/json", bytes.NewReader(raw)) + response, err := httpClient.Post(url, "application/json", bytes.NewReader(raw)) if err != nil { return errors.Wrap(err, "failed to post webhook") } - if response.StatusCode != http.StatusOK { - return statusCodeError{Code: response.StatusCode, Status: response.Status} - } - - return nil + return checkStatusCode(response, discard{}) } diff --git a/vendor/github.com/nlopes/slack/websocket.go b/vendor/github.com/nlopes/slack/websocket.go index 242acf40..122807b9 100644 --- a/vendor/github.com/nlopes/slack/websocket.go +++ b/vendor/github.com/nlopes/slack/websocket.go @@ -2,7 +2,7 @@ package slack import ( "encoding/json" - "errors" + "net/url" "sync" "time" @@ -20,6 +20,9 @@ const ( // // Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions) type RTM struct { + // Client is the main API, embedded + Client + idGen IDGenerator pingInterval time.Duration pingDeadman *time.Timer @@ -29,15 +32,10 @@ type RTM struct { IncomingEvents chan RTMEvent outgoingMessages chan OutgoingMessage killChannel chan bool - disconnected chan struct{} // disconnected is closed when Disconnect is invoked, regardless of connection state. Allows for ManagedConnection to not leak. + disconnected chan struct{} + disconnectedm *sync.Once forcePing chan bool rawEvents chan json.RawMessage - wasIntentional bool - isConnected bool - - // Client is the main API, embedded - Client - websocketURL string // UserDetails upon connection info *Info @@ -53,46 +51,35 @@ type RTM struct { // mu is mutex used to prevent RTM connection race conditions mu *sync.Mutex + + // connParams is a map of flags for connection parameters. + connParams url.Values } -// RTMOptions allows configuration of various options available for RTM messaging -// -// This structure will evolve in time so please make sure you are always using the -// named keys for every entry available as per Go 1 compatibility promise adding fields -// to this structure should not be considered a breaking change. -type RTMOptions struct { - // UseRTMStart set to true in order to use rtm.start or false to use rtm.connect - // As of 11th July 2017 you should prefer setting this to false, see: - // https://api.slack.com/changelog/2017-04-start-using-rtm-connect-and-stop-using-rtm-start - UseRTMStart bool +// signal that we are disconnected by closing the channel. +// protect it with a mutex to ensure it only happens once. +func (rtm *RTM) disconnect() { + rtm.disconnectedm.Do(func() { + close(rtm.disconnected) + }) } // Disconnect and wait, blocking until a successful disconnection. func (rtm *RTM) Disconnect() error { - // avoid RTM disconnect race conditions - rtm.mu.Lock() - defer rtm.mu.Unlock() - - // always push into the disconnected channel when invoked, + // always push into the kill channel when invoked, // this lets the ManagedConnection() function properly clean up. // if the buffer is full then just continue on. select { - case rtm.disconnected <- struct{}{}: - default: + case rtm.killChannel <- true: + return nil + case <-rtm.disconnected: + return ErrAlreadyDisconnected } - - if !rtm.isConnected { - return errors.New("Invalid call to Disconnect - Slack API is already disconnected") - } - - rtm.killChannel <- true - return nil } // GetInfo returns the info structure received when calling -// "startrtm", holding all channels, groups and other metadata needed -// to implement a full chat client. It will be non-nil after a call to -// StartRTM(). +// "startrtm", holding metadata needed to implement a full +// chat client. It will be non-nil after a call to StartRTM(). func (rtm *RTM) GetInfo() *Info { return rtm.info } @@ -110,7 +97,7 @@ func (rtm *RTM) SendMessage(msg *OutgoingMessage) { } func (rtm *RTM) resetDeadman() { - timerReset(rtm.pingDeadman, deadmanDuration(rtm.pingInterval)) + rtm.pingDeadman.Reset(deadmanDuration(rtm.pingInterval)) } func deadmanDuration(d time.Duration) time.Duration { diff --git a/vendor/github.com/nlopes/slack/websocket_internals.go b/vendor/github.com/nlopes/slack/websocket_internals.go index e8374b0d..3e1906ee 100644 --- a/vendor/github.com/nlopes/slack/websocket_internals.go +++ b/vendor/github.com/nlopes/slack/websocket_internals.go @@ -18,6 +18,7 @@ type ConnectedEvent struct { // ConnectionErrorEvent contains information about a connection error type ConnectionErrorEvent struct { Attempt int + Backoff time.Duration // how long we'll wait before the next attempt ErrorObj error } @@ -34,6 +35,7 @@ type ConnectingEvent struct { // DisconnectedEvent contains information about how we disconnected type DisconnectedEvent struct { Intentional bool + Cause error } // LatencyReport contains information about connection latency diff --git a/vendor/github.com/nlopes/slack/websocket_managed_conn.go b/vendor/github.com/nlopes/slack/websocket_managed_conn.go index e8ab65a1..8b3b3833 100644 --- a/vendor/github.com/nlopes/slack/websocket_managed_conn.go +++ b/vendor/github.com/nlopes/slack/websocket_managed_conn.go @@ -5,10 +5,13 @@ import ( "fmt" "io" "net/http" + stdurl "net/url" "reflect" "time" "github.com/gorilla/websocket" + "github.com/nlopes/slack/internal/errorsx" + "github.com/nlopes/slack/internal/timex" ) // ManageConnection can be called on a Slack RTM instance returned by the @@ -37,6 +40,7 @@ func (rtm *RTM) ManageConnection() { if info, conn, err = rtm.connect(connectionCount, rtm.useRTMStart); err != nil { // when the connection is unsuccessful its fatal, and we need to bail out. rtm.Debugf("Failed to connect with RTM on try %d: %s", connectionCount, err) + rtm.disconnect() return } @@ -44,7 +48,6 @@ func (rtm *RTM) ManageConnection() { // and conn. rtm.mu.Lock() rtm.conn = conn - rtm.isConnected = true rtm.info = info rtm.mu.Unlock() @@ -55,20 +58,19 @@ func (rtm *RTM) ManageConnection() { rtm.Debugf("RTM connection succeeded on try %d", connectionCount) - keepRunning := make(chan bool) - // we're now connected (or have failed fatally) so we can set up - // listeners - go rtm.handleIncomingEvents(keepRunning) + // we're now connected so we can set up listeners + go rtm.handleIncomingEvents() // this should be a blocking call until the connection has ended - rtm.handleEvents(keepRunning) + rtm.handleEvents() - // after being disconnected we need to check if it was intentional - // if not then we should try to reconnect - if rtm.wasIntentional { + select { + case <-rtm.disconnected: + // after handle events returns we need to check if we're disconnected return + default: + // otherwise continue and run the loop again to reconnect } - // else continue and run the loop again to connect } } @@ -87,18 +89,20 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke // used to provide exponential backoff wait time with jitter before trying // to connect to slack again boff := &backoff{ - Min: 100 * time.Millisecond, - Max: 5 * time.Minute, - Factor: 2, - Jitter: true, + Max: 5 * time.Minute, } for { + var ( + backoff time.Duration + ) + // send connecting event rtm.IncomingEvents <- RTMEvent{"connecting", &ConnectingEvent{ Attempt: boff.attempts + 1, ConnectionCount: connectionCount, }} + // attempt to start the connection info, conn, err := rtm.startRTMAndDial(useRTMStart) if err == nil { @@ -108,32 +112,49 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke // check for fatal errors switch err.Error() { case errInvalidAuth, errInactiveAccount, errMissingAuthToken: - rtm.Debugf("Invalid auth when connecting with RTM: %s", err) + rtm.Debugf("invalid auth when connecting with RTM: %s", err) rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}} return nil, nil, err default: } + switch actual := err.(type) { + case statusCodeError: + if actual.Code == http.StatusNotFound { + rtm.Debugf("invalid auth when connecting with RTM: %s", err) + rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}} + return nil, nil, err + } + case *RateLimitedError: + backoff = actual.RetryAfter + default: + } + + backoff = timex.Max(backoff, boff.Duration()) // any other errors are treated as recoverable and we try again after // sending the event along the IncomingEvents channel rtm.IncomingEvents <- RTMEvent{"connection_error", &ConnectionErrorEvent{ Attempt: boff.attempts, + Backoff: backoff, ErrorObj: err, }} - // check if Disconnect() has been invoked. + // get time we should wait before attempting to connect again + rtm.Debugf("reconnection %d failed: %s reconnecting in %v\n", boff.attempts, err, backoff) + + // wait for one of the following to occur, + // backoff duration has elapsed, killChannel is signalled, or + // the rtm finishes disconnecting. select { + case <-time.After(backoff): // retry after the backoff. + case intentional := <-rtm.killChannel: + if intentional { + rtm.killConnection(intentional, ErrRTMDisconnected) + return nil, nil, ErrRTMDisconnected + } case <-rtm.disconnected: - rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: true}} - return nil, nil, fmt.Errorf("disconnect received while trying to connect") - default: + return nil, nil, ErrRTMDisconnected } - - // get time we should wait before attempting to connect again - dur := boff.Duration() - rtm.Debugf("reconnection %d failed: %s", boff.attempts+1, err) - rtm.Debugln(" -> reconnecting in", dur) - time.Sleep(dur) } } @@ -157,6 +178,14 @@ func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn return nil, nil, err } + // install connection parameters + u, err := stdurl.Parse(url) + if err != nil { + return nil, nil, err + } + u.RawQuery = rtm.connParams.Encode() + url = u.String() + rtm.Debugf("Dialing to websocket on url %s", url) // Only use HTTPS for connections to prevent MITM attacks on the connection. upgradeHeader := http.Header{} @@ -178,15 +207,19 @@ func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn // // This should not be called directly! Instead a boolean value (true for // intentional, false otherwise) should be sent to the killChannel on the RTM. -func (rtm *RTM) killConnection(keepRunning chan bool, intentional bool) error { +func (rtm *RTM) killConnection(intentional bool, cause error) (err error) { rtm.Debugln("killing connection") - if rtm.isConnected { - close(keepRunning) + + if rtm.conn != nil { + err = rtm.conn.Close() + } + + rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: intentional, Cause: cause}} + + if intentional { + rtm.disconnect() } - rtm.isConnected = false - rtm.wasIntentional = intentional - err := rtm.conn.Close() - rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{intentional}} + return err } @@ -195,31 +228,28 @@ func (rtm *RTM) killConnection(keepRunning chan bool, intentional bool) error { // interval. This also sends outgoing messages that are received from the RTM's // outgoingMessages channel. This also handles incoming raw events from the RTM // rawEvents channel. -func (rtm *RTM) handleEvents(keepRunning chan bool) { +func (rtm *RTM) handleEvents() { ticker := time.NewTicker(rtm.pingInterval) defer ticker.Stop() for { select { // catch "stop" signal on channel close case intentional := <-rtm.killChannel: - _ = rtm.killConnection(keepRunning, intentional) + _ = rtm.killConnection(intentional, errorsx.String("signaled")) return - // detect when the connection is dead. case <-rtm.pingDeadman.C: - rtm.Debugln("deadman switch trigger disconnecting") - _ = rtm.killConnection(keepRunning, false) + _ = rtm.killConnection(false, errorsx.String("deadman switch triggered")) + return // send pings on ticker interval case <-ticker.C: - err := rtm.ping() - if err != nil { - _ = rtm.killConnection(keepRunning, false) + if err := rtm.ping(); err != nil { + _ = rtm.killConnection(false, err) return } case <-rtm.forcePing: - err := rtm.ping() - if err != nil { - _ = rtm.killConnection(keepRunning, false) + if err := rtm.ping(); err != nil { + _ = rtm.killConnection(false, err) return } // listen for messages that need to be sent @@ -229,7 +259,8 @@ func (rtm *RTM) handleEvents(keepRunning chan bool) { case rawEvent := <-rtm.rawEvents: switch rtm.handleRawEvent(rawEvent) { case rtmEventTypeGoodbye: - _ = rtm.killConnection(keepRunning, false) + _ = rtm.killConnection(false, errorsx.String("goodbye detected")) + return default: } } @@ -241,17 +272,10 @@ func (rtm *RTM) handleEvents(keepRunning chan bool) { // // This will stop executing once the RTM's keepRunning channel has been closed // or has anything sent to it. -func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) { +func (rtm *RTM) handleIncomingEvents() { for { - // non-blocking listen to see if channel is closed - select { - // catch "stop" signal on channel close - case <-keepRunning: + if err := rtm.receiveIncomingEvent(); err != nil { return - default: - if err := rtm.receiveIncomingEvent(); err != nil { - return - } } } } @@ -274,7 +298,7 @@ func (rtm *RTM) sendWithDeadline(msg interface{}) error { // and instead lets a future failed 'PING' detect the failed connection. func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) { rtm.Debugln("Sending message:", msg) - if len(msg.Text) > MaxMessageTextLength { + if len([]rune(msg.Text)) > MaxMessageTextLength { rtm.IncomingEvents <- RTMEvent{"outgoing_error", &MessageTooLongEvent{ Message: msg, MaxLength: MaxMessageTextLength, @@ -287,7 +311,6 @@ func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) { Message: msg, ErrorObj: err, }} - // TODO force ping? } } @@ -323,20 +346,32 @@ func (rtm *RTM) receiveIncomingEvent() error { // 'PING' message // trigger a 'PING' to detect potential websocket disconnect - rtm.forcePing <- true + select { + case rtm.forcePing <- true: + case <-rtm.disconnected: + } case err != nil: // All other errors from ReadJSON come from NextReader, and should // kill the read loop and force a reconnect. rtm.IncomingEvents <- RTMEvent{"incoming_error", &IncomingEventError{ ErrorObj: err, }} - rtm.killChannel <- false + + select { + case rtm.killChannel <- false: + case <-rtm.disconnected: + } + return err case len(event) == 0: rtm.Debugln("Received empty event") default: - rtm.Debugln("Incoming Event:", string(event[:])) - rtm.rawEvents <- event + rtm.Debugln("Incoming Event:", string(event)) + select { + case rtm.rawEvents <- event: + case <-rtm.disconnected: + rtm.Debugln("disonnected while attempting to send raw event") + } } return nil } @@ -405,8 +440,7 @@ func (rtm *RTM) handlePong(event json.RawMessage) { rtm.resetDeadman() if err := json.Unmarshal(event, &p); err != nil { - logger.Println("RTM Error unmarshalling 'pong' event:", err) - rtm.Debugln(" -> Erroneous 'ping' event:", string(event)) + rtm.Client.log.Println("RTM Error unmarshalling 'pong' event:", err) return } @@ -423,8 +457,8 @@ func (rtm *RTM) handlePong(event json.RawMessage) { func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { v, exists := EventMapping[typeStr] if !exists { - rtm.Debugf("RTM Error, received unmapped event %q: %s\n", typeStr, string(event)) - err := fmt.Errorf("RTM Error: Received unmapped event %q: %s\n", typeStr, string(event)) + rtm.Debugf("RTM Error - received unmapped event %q: %s\n", typeStr, string(event)) + err := fmt.Errorf("RTM Error: Received unmapped event %q: %s", typeStr, string(event)) rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} return } @@ -433,7 +467,7 @@ func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { err := json.Unmarshal(event, recvEvent) if err != nil { rtm.Debugf("RTM Error, could not unmarshall event %q: %s\n", typeStr, string(event)) - err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s\n", typeStr, string(event)) + err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s", typeStr, string(event)) rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} return } diff --git a/vendor/github.com/nlopes/slack/websocket_misc.go b/vendor/github.com/nlopes/slack/websocket_misc.go index 16f48c74..bfcc805e 100644 --- a/vendor/github.com/nlopes/slack/websocket_misc.go +++ b/vendor/github.com/nlopes/slack/websocket_misc.go @@ -43,9 +43,10 @@ type HelloEvent struct{} // PresenceChangeEvent represents the presence change event type PresenceChangeEvent struct { - Type string `json:"type"` - Presence string `json:"presence"` - User string `json:"user"` + Type string `json:"type"` + Presence string `json:"presence"` + User string `json:"user"` + Users []string `json:"users"` } // UserTypingEvent represents the user typing event diff --git a/vendor/modules.txt b/vendor/modules.txt index 90a2b316..b35b0212 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -142,14 +142,14 @@ github.com/klauspost/compress/zstd github.com/klauspost/compress/zstd/internal/xxhash # github.com/lib/pq v1.10.7 ## explicit; go 1.13 -# github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 -## explicit # github.com/nicklaw5/helix v1.25.0 ## explicit; go 1.15 github.com/nicklaw5/helix -# github.com/nlopes/slack v0.0.0-20180905213137-8cf10c586222 +# github.com/nlopes/slack v0.6.0 ## explicit github.com/nlopes/slack +github.com/nlopes/slack/internal/errorsx +github.com/nlopes/slack/internal/timex github.com/nlopes/slack/slackutilsx # github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d ## explicit