diff options
Diffstat (limited to 'vendor/github.com/nlopes')
56 files changed, 2039 insertions, 615 deletions
diff --git a/vendor/github.com/nlopes/slack/.travis.yml b/vendor/github.com/nlopes/slack/.travis.yml index e4b9c754..ed99d9ef 100644 --- a/vendor/github.com/nlopes/slack/.travis.yml +++ b/vendor/github.com/nlopes/slack/.travis.yml @@ -1,12 +1,9 @@ language: go -go: - - 1.7.x - - 1.8.x - - 1.9.x - - 1.10.x - - 1.11.x - - tip +env: + - GO111MODULE=on + +install: true before_install: - export PATH=$HOME/gopath/bin:$PATH @@ -20,6 +17,19 @@ script: matrix: 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 63309f23..48bcce55 100644 --- a/vendor/github.com/nlopes/slack/CHANGELOG.md +++ b/vendor/github.com/nlopes/slack/CHANGELOG.md @@ -1,3 +1,20 @@ +### 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.
diff --git a/vendor/github.com/nlopes/slack/Gopkg.lock b/vendor/github.com/nlopes/slack/Gopkg.lock deleted file mode 100644 index 9c33d0dc..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]] - name = "github.com/pkg/errors" - packages = ["."] - revision = "645ef00459ed84a119197bfb8d8205042c6df63d" - version = "v0.8.0" - -[[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 257870d6..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" - version = "0.8.0" - -[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 b4148231..a5e8e5ef 100644 --- a/vendor/github.com/nlopes/slack/README.md +++ b/vendor/github.com/nlopes/slack/README.md @@ -35,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 db44aa38..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 httpClient, method string, teamName string, values url.Values, d debug) (*adminResponse, error) { - adminResponse := &adminResponse{} - err := parseAdminResponse(ctx, client, method, teamName, values, adminResponse, d) +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) - 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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 06f59fa3..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. @@ -45,13 +50,6 @@ type AttachmentActionOptionGroup struct { // DEPRECATED: use InteractionCallback type AttachmentActionCallback InteractionCallback -// ActionCallback specific fields for the action callback. -type ActionCallback struct { - MessageTs string `json:"message_ts"` - AttachmentID string `json:"attachment_id"` - Actions []AttachmentAction `json:"actions"` -} - // ConfirmationField are used to ask users to confirm actions type ConfirmationField struct { Title string `json:"title,omitempty"` // Optional. diff --git a/vendor/github.com/nlopes/slack/auth.go b/vendor/github.com/nlopes/slack/auth.go index f8fe1f9d..dc1dbcdf 100644 --- a/vendor/github.com/nlopes/slack/auth.go +++ b/vendor/github.com/nlopes/slack/auth.go @@ -12,9 +12,9 @@ type AuthRevokeResponse struct { } // authRequest sends the actual request, and unmarshals the response -func authRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*AuthRevokeResponse, error) { +func (api *Client) authRequest(ctx context.Context, path string, values url.Values) (*AuthRevokeResponse, error) { response := &AuthRevokeResponse{} - err := postSlackMethod(ctx, client, path, values, response, d) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } @@ -36,5 +36,5 @@ func (api *Client) SendAuthRevokeContext(ctx context.Context, token string) (*Au "token": {token}, } - return authRequest(ctx, api.httpclient, "auth.revoke", values, api) + 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 <hr> 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 e27e76ab..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 httpClient, path string, values url.Values, d debug) (*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, d) + 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) + 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 711ae7c5..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,21 @@ 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 httpClient, path string, values url.Values, d debug) (*channelResponseFull, error) { +func (api *Client) channelRequest(ctx context.Context, path string, values url.Values) (*channelResponseFull, error) { response := &channelResponseFull{} - err := postForm(ctx, client, APIURL+path, values, response, d) + 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, nil + + return response, response.Err() } type channelsConfig struct { @@ -75,7 +72,7 @@ func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) "channel": {channelID}, } - _, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api) + _, err = api.channelRequest(ctx, "channels.archive", values) return err } @@ -93,7 +90,7 @@ func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string "channel": {channelID}, } - _, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api) + _, err = api.channelRequest(ctx, "channels.unarchive", values) return err } @@ -111,7 +108,7 @@ func (api *Client) CreateChannelContext(ctx context.Context, channelName string) "name": {channelName}, } - response, err := channelRequest(ctx, api.httpclient, "channels.create", values, api) + response, err := api.channelRequest(ctx, "channels.create", values) if err != nil { return nil, err } @@ -156,7 +153,7 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID strin } } - response, err := channelRequest(ctx, api.httpclient, "channels.history", values, api) + response, err := api.channelRequest(ctx, "channels.history", values) if err != nil { return nil, err } @@ -178,7 +175,7 @@ func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) "include_locale": {strconv.FormatBool(true)}, } - response, err := channelRequest(ctx, api.httpclient, "channels.info", values, api) + response, err := api.channelRequest(ctx, "channels.info", values) if err != nil { return nil, err } @@ -200,7 +197,7 @@ func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, us "user": {user}, } - response, err := channelRequest(ctx, api.httpclient, "channels.invite", values, api) + response, err := api.channelRequest(ctx, "channels.invite", values) if err != nil { return nil, err } @@ -221,7 +218,7 @@ func (api *Client) JoinChannelContext(ctx context.Context, channelName string) ( "name": {channelName}, } - response, err := channelRequest(ctx, api.httpclient, "channels.join", values, api) + response, err := api.channelRequest(ctx, "channels.join", values) if err != nil { return nil, err } @@ -242,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) + response, err := api.channelRequest(ctx, "channels.leave", values) if err != nil { return false, err } @@ -265,7 +262,7 @@ func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, us "user": {user}, } - _, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api) + _, err = api.channelRequest(ctx, "channels.kick", values) return err } @@ -283,6 +280,7 @@ func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool, "token": {api.token}, }, } + if excludeArchived { options = append(options, GetChannelsOptionExcludeArchived()) } @@ -293,7 +291,7 @@ func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool, } } - response, err := channelRequest(ctx, api.httpclient, "channels.list", config.values, api) + response, err := api.channelRequest(ctx, "channels.list", config.values) if err != nil { return nil, err } @@ -320,7 +318,7 @@ func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts "ts": {ts}, } - _, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api) + _, err = api.channelRequest(ctx, "channels.mark", values) return err } @@ -341,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) + response, err := api.channelRequest(ctx, "channels.rename", values) if err != nil { return nil, err } @@ -363,7 +361,7 @@ func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purp "purpose": {purpose}, } - response, err := channelRequest(ctx, api.httpclient, "channels.setPurpose", values, api) + response, err := api.channelRequest(ctx, "channels.setPurpose", values) if err != nil { return "", err } @@ -385,7 +383,7 @@ func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic "topic": {topic}, } - response, err := channelRequest(ctx, api.httpclient, "channels.setTopic", values, api) + response, err := api.channelRequest(ctx, "channels.setTopic", values) if err != nil { return "", err } @@ -406,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) + 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 c08c1808..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 @@ -156,17 +157,18 @@ 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 = postForm(ctx, api.httpclient, config.endpoint, config.values, &response, api); err != nil { + if err = doPost(ctx, api.httpclient, req, parser(&response), api); err != nil { return "", "", "", err } @@ -176,14 +178,15 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt // 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 string, options ...MsgOption) (string, url.Values, error) { - config, err := applyMsgOptions(token, channel, options...) +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{ - endpoint: APIURL + string(chatPostMessage), + apiurl: apiurl, + endpoint: apiurl + string(chatPostMessage), values: url.Values{ "token": {token}, "channel": {channel}, @@ -199,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 ( @@ -206,22 +216,77 @@ 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 { + 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. type MsgOption func(*sendConfig) error // MsgOptionPost posts a messages, this is the default. func MsgOptionPost() MsgOption { return func(config *sendConfig) error { - config.endpoint = APIURL + string(chatPostMessage) + config.endpoint = config.apiurl + string(chatPostMessage) config.values.Del("ts") return nil } @@ -230,7 +295,7 @@ func MsgOptionPost() MsgOption { // MsgOptionPostEphemeral - posts an ephemeral message to the provided user. func MsgOptionPostEphemeral(userID string) MsgOption { return func(config *sendConfig) error { - config.endpoint = APIURL + string(chatPostEphemeral) + config.endpoint = config.apiurl + string(chatPostEphemeral) MsgOptionUser(userID)(config) config.values.Del("ts") @@ -241,7 +306,7 @@ func MsgOptionPostEphemeral(userID string) MsgOption { // MsgOptionMeMessage posts a "me message" type from the calling user func MsgOptionMeMessage() MsgOption { return func(config *sendConfig) error { - config.endpoint = APIURL + string(chatMeMessage) + config.endpoint = config.apiurl + string(chatMeMessage) return nil } } @@ -249,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.endpoint = APIURL + string(chatUpdate) + config.endpoint = config.apiurl + string(chatUpdate) config.values.Add("ts", timestamp) return nil } @@ -258,7 +323,7 @@ func MsgOptionUpdate(timestamp string) MsgOption { // MsgOptionDelete deletes a message based on the timestamp. func MsgOptionDelete(timestamp string) MsgOption { return func(config *sendConfig) error { - config.endpoint = APIURL + string(chatDelete) + config.endpoint = config.apiurl + string(chatDelete) config.values.Add("ts", timestamp) return nil } @@ -267,7 +332,7 @@ func MsgOptionDelete(timestamp string) MsgOption { // MsgOptionUnfurl unfurls a message based on the timestamp. func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption { return func(config *sendConfig) error { - config.endpoint = APIURL + string(chatUnfurl) + config.endpoint = config.apiurl + string(chatUnfurl) config.values.Add("ts", timestamp) unfurlsStr, err := json.Marshal(unfurls) if err == nil { @@ -277,6 +342,17 @@ func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption } } +// 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 + } +} + // MsgOptionAsUser whether or not to send the message as the user. func MsgOptionAsUser(b bool) MsgOption { return func(config *sendConfig) error { @@ -322,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(attachments)) + 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("blocks", string(blocks)) } return err } @@ -395,15 +493,31 @@ 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 @@ -499,7 +613,7 @@ func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkPar Permalink string `json:"permalink"` SlackResponse }{} - err := getSlackMethod(ctx, api.httpclient, "chat.getPermalink", values, &response, api) + err := api.getMethod(ctx, "chat.getPermalink", values, &response) if err != nil { return "", err } diff --git a/vendor/github.com/nlopes/slack/conversation.go b/vendor/github.com/nlopes/slack/conversation.go index ccd38f88..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"` @@ -67,10 +66,11 @@ type GetUsersInConversationParameters struct { } type GetConversationsForUserParameters struct { - UserID string - Cursor string - Types []string - Limit int + UserID string + Cursor string + Types []string + Limit int + ExcludeArchived bool } type responseMetaData struct { @@ -99,13 +99,16 @@ 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) + + 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 } @@ -131,12 +134,15 @@ func (api *Client) GetConversationsForUserContext(ctx context.Context, params *G 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 = postSlackMethod(ctx, api.httpclient, "users.conversations", values, &response, api) + err = api.postMethod(ctx, "users.conversations", values, &response) if err != nil { return nil, "", err } @@ -155,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) + err := api.postMethod(ctx, "conversations.archive", values, &response) if err != nil { return err } @@ -176,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) + err := api.postMethod(ctx, "conversations.unarchive", values, &response) if err != nil { return err } @@ -200,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) + err := api.postMethod(ctx, "conversations.setTopic", values, &response) if err != nil { return nil, err } @@ -224,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) + + err := api.postMethod(ctx, "conversations.setPurpose", values, &response) if err != nil { return nil, err } @@ -248,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) + + err := api.postMethod(ctx, "conversations.rename", values, &response) if err != nil { return nil, err } @@ -272,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) + + err := api.postMethod(ctx, "conversations.invite", values, &response) if err != nil { return nil, err } @@ -292,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) + err := api.postMethod(ctx, "conversations.kick", values, &response) if err != nil { return err } @@ -318,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) + err = api.postMethod(ctx, "conversations.close", values, &response) if err != nil { return false, false, err } @@ -338,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) + 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 @@ -359,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) + response, err := api.channelRequest(ctx, "conversations.info", values) if err != nil { return nil, err } @@ -380,7 +389,7 @@ func (api *Client) LeaveConversationContext(ctx context.Context, channelID strin "channel": {channelID}, } - response, err := channelRequest(ctx, api.httpclient, "conversations.leave", values, api) + response, err := api.channelRequest(ctx, "conversations.leave", values) if err != nil { return false, err } @@ -436,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) + err = api.postMethod(ctx, "conversations.replies", values, &response) if err != nil { return nil, false, "", err } @@ -476,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) + + err = api.postMethod(ctx, "conversations.list", values, &response) if err != nil { return nil, "", err } @@ -513,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) + + err := api.postMethod(ctx, "conversations.open", values, &response) if err != nil { return nil, false, false, err } @@ -537,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) + + err := api.postMethod(ctx, "conversations.join", values, &response) if err != nil { return nil, "", nil, err } @@ -599,7 +611,7 @@ func (api *Client) GetConversationHistoryContext(ctx context.Context, params *Ge response := GetConversationHistoryResponse{} - err := postSlackMethod(ctx, api.httpclient, "conversations.history", values, &response, api) + err := api.postMethod(ctx, "conversations.history", values, &response) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/dialog.go b/vendor/github.com/nlopes/slack/dialog.go index 7b9e3814..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 @@ -25,6 +25,7 @@ type DialogInput struct { Name string `json:"name"` Placeholder string `json:"placeholder"` Optional bool `json:"optional"` + Hint string `json:"hint"` } // DialogTrigger ... @@ -89,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{ @@ -103,10 +104,15 @@ func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dial } response := &DialogOpenResponse{} - endpoint := APIURL + "dialog.open" + 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 ea95ccfa..385cef68 100644 --- a/vendor/github.com/nlopes/slack/dialog_select.go +++ b/vendor/github.com/nlopes/slack/dialog_select.go @@ -21,10 +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 @@ -54,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, @@ -69,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 da6e4a16..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 httpClient, path string, values url.Values, d debug) (*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, d) + 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); 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) + 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) + response, err := api.dndRequest(ctx, "dnd.info", values) if err != nil { return nil, err } @@ -120,13 +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); err != nil { + if err := api.postMethod(ctx, "dnd.teamInfo", values, response); err != nil { return nil, err } if response.Err() != nil { return nil, response.Err() } + return response.Users, nil } @@ -137,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{ @@ -145,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) + 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 aed2129f..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) + 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 136ea266..3a7363de 100644 --- a/vendor/github.com/nlopes/slack/files.go +++ b/vendor/github.com/nlopes/slack/files.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "fmt" "io" "net/url" @@ -91,7 +90,8 @@ type File struct { } type Share struct { - Public map[string][]ShareFileInfo `json:"public"` + Public map[string][]ShareFileInfo `json:"public"` + Private map[string][]ShareFileInfo `json:"private"` } type ShareFileInfo struct { @@ -134,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 } @@ -156,9 +166,9 @@ func NewGetFilesParameters() GetFilesParameters { } } -func fileRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*fileResponseFull, error) { +func (api *Client) fileRequest(ctx context.Context, path string, values url.Values) (*fileResponseFull, error) { response := &fileResponseFull{} - err := postForm(ctx, client, APIURL+path, values, response, d) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } @@ -180,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) + 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{ @@ -219,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) + response, err := api.fileRequest(ctx, "files.list", values) if err != nil { return nil, nil, err } @@ -239,9 +288,6 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam if err != nil { return nil, err } - if params.Filename == "" { - return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory") - } response := &fileResponseFull{} values := url.Values{ "token": {api.token}, @@ -266,12 +312,16 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam } if params.Content != "" { values.Add("content", params.Content) - err = postForm(ctx, api.httpclient, APIURL+"files.upload", values, response, api) + 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) + 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) + 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 } @@ -287,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{ @@ -295,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) + _, err = api.fileRequest(ctx, "files.comments.delete", values) return err } @@ -311,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) + _, err = api.fileRequest(ctx, "files.delete", values) return err } @@ -327,7 +377,7 @@ func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string "file": {fileID}, } - response, err := fileRequest(ctx, api.httpclient, "files.revokePublicURL", values, api) + response, err := api.fileRequest(ctx, "files.revokePublicURL", values) if err != nil { return nil, err } @@ -346,7 +396,7 @@ func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) "file": {fileID}, } - response, err := fileRequest(ctx, api.httpclient, "files.sharedPublicURL", values, api) + response, err := api.fileRequest(ctx, "files.sharedPublicURL", values) if err != nil { return nil, nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/go.mod b/vendor/github.com/nlopes/slack/go.mod new file mode 100644 index 00000000..87256eb1 --- /dev/null +++ b/vendor/github.com/nlopes/slack/go.mod @@ -0,0 +1,9 @@ +module github.com/nlopes/slack + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gorilla/websocket v1.2.0 + github.com/pkg/errors v0.8.0 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.2.2 +) diff --git a/vendor/github.com/nlopes/slack/go.sum b/vendor/github.com/nlopes/slack/go.sum new file mode 100644 index 00000000..3bb45c1f --- /dev/null +++ b/vendor/github.com/nlopes/slack/go.sum @@ -0,0 +1,22 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= +github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/nlopes/slack v0.1.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= +github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0= +github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/victorcoder/slack-test v0.0.0-20190131110821-6f9a569c10af h1:JFxr+No3ZWgCtxnnTWCybnB/z0Iy3qLmdj3u2NV5o48= +github.com/victorcoder/slack-test v0.0.0-20190131110821-6f9a569c10af/go.mod h1:dStM4ShMus8J3hiq66ExbbzGLkwyZ+RQJePwFhWCCvQ= +github.com/victorcoder/slack-test v0.0.0-20190131113129-a43b3bb77f43 h1:wtFekkaAAQibpy3iE4Hhx2Gi9pZAbITOSfVP7GXk5eM= +github.com/victorcoder/slack-test v0.0.0-20190131113129-a43b3bb77f43/go.mod h1:dStM4ShMus8J3hiq66ExbbzGLkwyZ+RQJePwFhWCCvQ= +golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37 h1:BkNcmLtAVeWe9h5k0jt24CQgaG5vb4x/doFbAiEC/Ho= +golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/vendor/github.com/nlopes/slack/groups.go b/vendor/github.com/nlopes/slack/groups.go index 560faee9..23374869 100644 --- a/vendor/github.com/nlopes/slack/groups.go +++ b/vendor/github.com/nlopes/slack/groups.go @@ -8,7 +8,7 @@ import ( // Group contains all the information for a group type Group struct { - groupConversation + GroupConversation IsGroup bool `json:"is_group"` } @@ -27,9 +27,9 @@ type groupResponseFull struct { SlackResponse } -func groupRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*groupResponseFull, error) { +func (api *Client) groupRequest(ctx context.Context, path string, values url.Values) (*groupResponseFull, error) { response := &groupResponseFull{} - err := postForm(ctx, client, APIURL+path, values, response, d) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } @@ -49,7 +49,7 @@ func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error "channel": {group}, } - _, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api) + _, err := api.groupRequest(ctx, "groups.archive", values) return err } @@ -65,7 +65,7 @@ func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) erro "channel": {group}, } - _, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api) + _, err := api.groupRequest(ctx, "groups.unarchive", values) return err } @@ -81,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) + response, err := api.groupRequest(ctx, "groups.create", values) if err != nil { return nil, err } @@ -106,7 +106,7 @@ func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (* "channel": {group}, } - response, err := groupRequest(ctx, api.httpclient, "groups.createChild", values, api) + response, err := api.groupRequest(ctx, "groups.createChild", values) if err != nil { return nil, err } @@ -148,7 +148,7 @@ func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, par } } - response, err := groupRequest(ctx, api.httpclient, "groups.history", values, api) + response, err := api.groupRequest(ctx, "groups.history", values) if err != nil { return nil, err } @@ -168,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) + response, err := api.groupRequest(ctx, "groups.invite", values) if err != nil { return nil, false, err } @@ -187,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) + _, err = api.groupRequest(ctx, "groups.leave", values) return err } @@ -204,7 +204,7 @@ func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user str "user": {user}, } - _, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api) + _, err = api.groupRequest(ctx, "groups.kick", values) return err } @@ -222,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) + response, err := api.groupRequest(ctx, "groups.list", values) if err != nil { return nil, err } @@ -242,7 +242,7 @@ func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Grou "include_locale": {strconv.FormatBool(true)}, } - response, err := groupRequest(ctx, api.httpclient, "groups.info", values, api) + response, err := api.groupRequest(ctx, "groups.info", values) if err != nil { return nil, err } @@ -267,7 +267,7 @@ func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string "ts": {ts}, } - _, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api) + _, err = api.groupRequest(ctx, "groups.mark", values) return err } @@ -283,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) + response, err := api.groupRequest(ctx, "groups.open", values) if err != nil { return false, false, err } @@ -307,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) + response, err := api.groupRequest(ctx, "groups.rename", values) if err != nil { return nil, err } @@ -327,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) + response, err := api.groupRequest(ctx, "groups.setPurpose", values) if err != nil { return "", err } @@ -347,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) + 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 10563d91..ee784fef 100644 --- a/vendor/github.com/nlopes/slack/im.go +++ b/vendor/github.com/nlopes/slack/im.go @@ -22,15 +22,13 @@ 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 httpClient, path string, values url.Values, d debug) (*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, d) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } @@ -50,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) + response, err := api.imRequest(ctx, "im.close", values) if err != nil { return false, false, err } @@ -71,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) + response, err := api.imRequest(ctx, "im.open", values) if err != nil { return false, false, "", err } @@ -91,7 +89,7 @@ func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) "ts": {ts}, } - _, err := imRequest(ctx, api.httpclient, "im.mark", values, api) + _, err := api.imRequest(ctx, "im.mark", values) return err } @@ -130,7 +128,7 @@ func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, para } } - response, err := imRequest(ctx, api.httpclient, "im.history", values, api) + response, err := api.imRequest(ctx, "im.history", values) if err != nil { return nil, err } @@ -148,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) + 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 index addc2864..5433463d 100644 --- a/vendor/github.com/nlopes/slack/interactions.go +++ b/vendor/github.com/nlopes/slack/interactions.go @@ -1,8 +1,20 @@ 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") @@ -10,6 +22,7 @@ const ( 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. @@ -27,6 +40,59 @@ type InteractionCallback struct { Message Message `json:"message"` Name string `json:"name"` Value string `json:"value"` - ActionCallback + 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/messages.go b/vendor/github.com/nlopes/slack/messages.go index bde9a37e..37a26335 100644 --- a/vendor/github.com/nlopes/slack/messages.go +++ b/vendor/github.com/nlopes/slack/messages.go @@ -16,6 +16,7 @@ type OutgoingMessage struct { type Message struct { Msg SubMessage *Msg `json:"message,omitempty"` + PreviousMessage *Msg `json:"previous_message,omitempty"` } // Msg contains information about a slack message @@ -92,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"` diff --git a/vendor/github.com/nlopes/slack/misc.go b/vendor/github.com/nlopes/slack/misc.go index 30ae4628..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" @@ -48,31 +49,95 @@ 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 (e *RateLimitedError) Retryable() bool { + return true } 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 { + return nil, err + } 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 { + return err + } + + 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.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 } @@ -89,7 +154,7 @@ func parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error { return json.Unmarshal(response, intf) } -func postLocalWithMultipartResponse(ctx context.Context, client httpClient, path, fpath, fieldname string, values url.Values, intf interface{}, d debug) 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 @@ -99,7 +164,8 @@ func postLocalWithMultipartResponse(ctx context.Context, client httpClient, path return err } defer file.Close() - return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, d) + + return postWithMultipartResponse(ctx, client, method, filepath.Base(fpath), fieldname, values, file, intf, d) } func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, d debug) error { @@ -123,7 +189,7 @@ func postWithMultipartResponse(ctx context.Context, client httpClient, path, nam return } }() - req, err := fileUploadReq(ctx, APIURL+path, values, pipeReader) + req, err := fileUploadReq(ctx, path, values, pipeReader) if err != nil { return err } @@ -136,28 +202,20 @@ func postWithMultipartResponse(ctx context.Context, client httpClient, path, nam } 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, d) - return statusCodeError{Code: resp.StatusCode, Status: resp.Status} - } select { case err = <-errc: return err default: - return parseResponseBody(resp.Body, intf, d) + return newJSONParser(intf)(resp) } } -func doPost(ctx context.Context, client httpClient, req *http.Request, intf interface{}, d debug) 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 { @@ -165,21 +223,12 @@ func doPost(ctx context.Context, client httpClient, req *http.Request, intf inte } 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, d) - return statusCodeError{Code: resp.StatusCode, Status: resp.Status} + err = checkStatusCode(resp, d) + if err != nil { + return err } - return parseResponseBody(resp.Body, intf, d) + return parser(resp) } // post JSON. @@ -191,7 +240,8 @@ func postJSON(ctx context.Context, client httpClient, endpoint, token string, js } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - return doPost(ctx, client, req, intf, d) + + return doPost(ctx, client, req, newJSONParser(intf), d) } // post a url encoded form. @@ -202,17 +252,7 @@ func postForm(ctx context.Context, client httpClient, endpoint string, values ur return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - return doPost(ctx, client, req, intf, d) -} - -// post to a slack web method. -func postSlackMethod(ctx context.Context, client httpClient, path string, values url.Values, intf interface{}, d debug) error { - return postForm(ctx, client, APIURL+path, values, intf, d) -} - -// get a slack web method. -func getSlackMethod(ctx context.Context, client httpClient, path string, values url.Values, intf interface{}, d debug) error { - return getResource(ctx, client, APIURL+path, values, intf, d) + return doPost(ctx, client, req, newJSONParser(intf), d) } func getResource(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error { @@ -223,7 +263,7 @@ func getResource(ctx context.Context, client httpClient, endpoint string, values req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.URL.RawQuery = values.Encode() - return doPost(ctx, client, req, intf, d) + return doPost(ctx, client, req, newJSONParser(intf), d) } func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf interface{}, d debug) error { @@ -251,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() { @@ -264,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 8a8194cb..29d6dce9 100644 --- a/vendor/github.com/nlopes/slack/oauth.go +++ b/vendor/github.com/nlopes/slack/oauth.go @@ -57,7 +57,7 @@ func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, c "redirect_uri": {redirectURI}, } response := &OAuthResponse{} - if err = postSlackMethod(ctx, client, "oauth.access", values, response, discard{}); err != nil { + if err = postForm(ctx, client, APIURL+"oauth.access", values, response, discard{}); err != nil { return nil, err } return response, response.Err() diff --git a/vendor/github.com/nlopes/slack/pins.go b/vendor/github.com/nlopes/slack/pins.go index c1d525df..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); 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); 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) + 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 abe1e72a..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); 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); 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); 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) + 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 index 54c91789..9b905387 100644 --- a/vendor/github.com/nlopes/slack/reminders.go +++ b/vendor/github.com/nlopes/slack/reminders.go @@ -23,7 +23,7 @@ type reminderResp struct { func (api *Client) doReminder(ctx context.Context, path string, values url.Values) (*Reminder, error) { response := &reminderResp{} - if err := postSlackMethod(ctx, api.httpclient, path, values, response, api); err != nil { + if err := api.postMethod(ctx, path, values, response); err != nil { return nil, err } return &response.Reminder, response.Err() @@ -68,7 +68,7 @@ func (api *Client) DeleteReminder(id string) error { "reminder": {id}, } response := &SlackResponse{} - if err := postSlackMethod(context.Background(), api.httpclient, "reminders.delete", values, response, api); err != nil { + 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 e7fa83f7..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) + 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) + 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 @@ -112,14 +112,13 @@ func RTMOptionConnParams(connParams url.Values) RTMOption { func (api *Client) NewRTM(options ...RTMOption) *RTM { result := &RTM{ Client: *api, - wasIntentional: true, - isConnected: false, IncomingEvents: make(chan RTMEvent, 50), outgoingMessages: make(chan OutgoingMessage, 20), pingInterval: defaultPingInterval, pingDeadman: time.NewTimer(deadmanDuration(defaultPingInterval)), 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), diff --git a/vendor/github.com/nlopes/slack/search.go b/vendor/github.com/nlopes/slack/search.go index 2d018fcc..67e3b1d1 100644 --- a/vendor/github.com/nlopes/slack/search.go +++ b/vendor/github.com/nlopes/slack/search.go @@ -41,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"` @@ -103,7 +104,7 @@ func (api *Client) _search(ctx context.Context, path, query string, params Searc } response = &searchResponseFull{} - err := postSlackMethod(ctx, api.httpclient, path, values, response, api) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/security.go b/vendor/github.com/nlopes/slack/security.go index 35727027..dbe8fb2d 100644 --- a/vendor/github.com/nlopes/slack/security.go +++ b/vendor/github.com/nlopes/slack/security.go @@ -4,7 +4,6 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/hex" - "errors" "fmt" "hash" "net/http" @@ -34,7 +33,7 @@ func unsafeSignatureVerifier(header http.Header, secret string) (_ SecretsVerifi stimestamp := header.Get(hTimestamp) if signature == "" || stimestamp == "" { - return SecretsVerifier{}, errors.New("missing headers") + return SecretsVerifier{}, ErrMissingHeaders } if bsignature, err = hex.DecodeString(strings.TrimPrefix(signature, "v0=")); err != nil { @@ -70,7 +69,7 @@ func NewSecretsVerifier(header http.Header, secret string) (sv SecretsVerifier, diff := absDuration(time.Since(time.Unix(timestamp, 0))) if diff > 5*time.Minute { - return SecretsVerifier{}, fmt.Errorf("timestamp is too old") + return SecretsVerifier{}, ErrExpiredTimestamp } return sv, err @@ -88,7 +87,7 @@ func (v SecretsVerifier) Ensure() error { return nil } - return fmt.Errorf("Expected signing signature: %s, but computed: %s", v.signature, computed) + return fmt.Errorf("Expected signing signature: %s, but computed: %s", hex.EncodeToString(v.signature), hex.EncodeToString(computed)) } func abs64(n int64) int64 { diff --git a/vendor/github.com/nlopes/slack/slack.go b/vendor/github.com/nlopes/slack/slack.go index c1ba0fc3..94230526 100644 --- a/vendor/github.com/nlopes/slack/slack.go +++ b/vendor/github.com/nlopes/slack/slack.go @@ -9,11 +9,12 @@ import ( "os" ) -// APIURL added as a var so that we can change this for testing purposes -var APIURL = "https://slack.com/api/" - -// WEBAPIURLFormat ... -const WEBAPIURLFormat = "https://%s.slack.com/api/users.admin.%s?t=%d" +const ( + // APIURL of the slack api. + APIURL = "https://slack.com/api/" + // WEBAPIURLFormat ... + WEBAPIURLFormat = "https://%s.slack.com/api/users.admin.%s?t=%d" +) // httpClient defines the minimal interface needed for an http.Client to be implemented. type httpClient interface { @@ -40,6 +41,8 @@ type AuthTestResponse struct { 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 { @@ -48,8 +51,11 @@ type authTestResponseFull struct { } // Client for the slack api. +type ParamOption func(*url.Values) + type Client struct { token string + endpoint string debug bool log ilogger httpclient httpClient @@ -79,10 +85,16 @@ func OptionLog(l logger) func(*Client) { } } +// 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, + endpoint: APIURL, httpclient: &http.Client{}, log: log.New(os.Stderr, "nlopes/slack", log.LstdFlags|log.Lshortfile), } @@ -103,7 +115,7 @@ func (api *Client) AuthTest() (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) + err = api.postMethod(ctx, "auth.test", url.Values{"token": {api.token}}, responseFull) if err != nil { return nil, err } @@ -129,3 +141,13 @@ func (api *Client) Debugln(v ...interface{}) { 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 7e1e621d..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); 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); 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) + 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 1892cf5f..029e2b5b 100644 --- a/vendor/github.com/nlopes/slack/team.go +++ b/vendor/github.com/nlopes/slack/team.go @@ -66,9 +66,9 @@ func NewAccessLogParameters() AccessLogParameters { } } -func teamRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*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, d) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } @@ -76,9 +76,9 @@ func teamRequest(ctx context.Context, client httpClient, path string, values url return response, response.Err() } -func billableInfoRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (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, d) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } @@ -86,9 +86,9 @@ func billableInfoRequest(ctx context.Context, client httpClient, path string, va return response.BillableInfo, response.Err() } -func accessLogsRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*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, d) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } @@ -106,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) + response, err := api.teamRequest(ctx, "team.info", values) if err != nil { return nil, err } @@ -130,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) + 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) + return api.billableInfoRequest(ctx, "team.billableInfo", values) } // GetBillableInfoForTeam returns the billing_active status of all users on the team. @@ -161,5 +163,5 @@ func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[strin "token": {api.token}, } - return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api) + 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 9e145272..f3206591 100644 --- a/vendor/github.com/nlopes/slack/usergroups.go +++ b/vendor/github.com/nlopes/slack/usergroups.go @@ -40,9 +40,9 @@ type userGroupResponseFull struct { SlackResponse } -func userGroupRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*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, d) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } @@ -74,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) + response, err := api.userGroupRequest(ctx, "usergroups.create", values) if err != nil { return UserGroup{}, err } @@ -93,7 +93,7 @@ func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string "usergroup": {userGroup}, } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.disable", values, api) + response, err := api.userGroupRequest(ctx, "usergroups.disable", values) if err != nil { return UserGroup{}, err } @@ -112,7 +112,7 @@ func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) "usergroup": {userGroup}, } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.enable", values, api) + response, err := api.userGroupRequest(ctx, "usergroups.enable", values) if err != nil { return UserGroup{}, err } @@ -176,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) + response, err := api.userGroupRequest(ctx, "usergroups.list", values) if err != nil { return nil, err } @@ -206,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) + response, err := api.userGroupRequest(ctx, "usergroups.update", values) if err != nil { return UserGroup{}, err } @@ -226,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) + response, err := api.userGroupRequest(ctx, "usergroups.users.list", values) if err != nil { return []string{}, err } @@ -246,7 +250,7 @@ func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup "users": {members}, } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.update", values, api) + 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 74b79372..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,9 +203,9 @@ func NewUserSetPhotoParams() UserSetPhotoParams { } } -func userRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*userResponseFull, error) { +func (api *Client) userRequest(ctx context.Context, path string, values url.Values) (*userResponseFull, error) { response := &userResponseFull{} - err := postForm(ctx, client, APIURL+path, values, response, d) + err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } @@ -211,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) + response, err := api.userRequest(ctx, "users.getPresence", values) if err != nil { return nil, err } @@ -231,7 +245,7 @@ func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, "include_locale": {strconv.FormatBool(true)}, } - response, err := userRequest(ctx, api.httpclient, "users.info", values, api) + response, err := api.userRequest(ctx, "users.info", values) if err != nil { return nil, err } @@ -310,7 +324,7 @@ func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) "include_locale": {strconv.FormatBool(true)}, } - if resp, err = userRequest(ctx, t.c.httpclient, "users.list", values, t.c); 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) + 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) + _, 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) + _, 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, APIURL+"users.identity", values, response, api) + 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) + 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, APIURL+"users.deletePhoto", values, response, api) + 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, APIURL+"users.profile.set", values, response, api); 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) + 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 e5dee68a..122807b9 100644 --- a/vendor/github.com/nlopes/slack/websocket.go +++ b/vendor/github.com/nlopes/slack/websocket.go @@ -2,7 +2,6 @@ package slack import ( "encoding/json" - "errors" "net/url" "sync" "time" @@ -33,11 +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 // UserDetails upon connection info *Info @@ -58,32 +56,30 @@ type RTM struct { connParams url.Values } +// 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 } 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 62157910..8b3b3833 100644 --- a/vendor/github.com/nlopes/slack/websocket_managed_conn.go +++ b/vendor/github.com/nlopes/slack/websocket_managed_conn.go @@ -10,6 +10,8 @@ import ( "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 @@ -38,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 } @@ -45,7 +48,6 @@ func (rtm *RTM) ManageConnection() { // and conn. rtm.mu.Lock() rtm.conn = conn - rtm.isConnected = true rtm.info = info rtm.mu.Unlock() @@ -56,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 } } @@ -88,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 { @@ -109,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) } } @@ -187,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 } @@ -204,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 @@ -238,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: } } @@ -250,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 - } } } } @@ -296,7 +311,6 @@ func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) { Message: msg, ErrorObj: err, }} - // TODO force ping? } } @@ -332,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 } |