4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-07-03 11:57:45 +00:00

Update dependencies (#886)

This commit is contained in:
Wim
2019-09-07 22:46:58 +02:00
committed by GitHub
parent 1dc93ec4f0
commit a3bee01e0a
145 changed files with 24283 additions and 16572 deletions

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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)
}

View File

@ -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

71
vendor/github.com/nlopes/slack/block.go generated vendored Normal file
View File

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

26
vendor/github.com/nlopes/slack/block_action.go generated vendored Normal file
View File

@ -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,
},
}
}

32
vendor/github.com/nlopes/slack/block_context.go generated vendored Normal file
View File

@ -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,
}
}

303
vendor/github.com/nlopes/slack/block_conv.go generated vendored Normal file
View File

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

22
vendor/github.com/nlopes/slack/block_divider.go generated vendored Normal file
View File

@ -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,
}
}

238
vendor/github.com/nlopes/slack/block_element.go generated vendored Normal file
View File

@ -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,
}
}

28
vendor/github.com/nlopes/slack/block_image.go generated vendored Normal file
View File

@ -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,
}
}

216
vendor/github.com/nlopes/slack/block_object.go generated vendored Normal file
View File

@ -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,
}
}

42
vendor/github.com/nlopes/slack/block_section.go generated vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
}

View File

@ -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,
}
OptionGroups: options}
}
func optionsFromArray(options []string) []DialogSelectOption {
selectOptions := make([]DialogSelectOption, len(options))
for idx, value := range options {
selectOptions[idx] = DialogSelectOption{
Label: value,
Value: value,
}
// NewDialogOptionGroup creates a DialogOptionGroup from several select options
func NewDialogOptionGroup(label string, options ...DialogSelectOption) DialogOptionGroup {
return DialogOptionGroup{
Label: label,
Options: options,
}
return selectOptions
}
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++
}
return selectOptions
}
// NewConversationsSelect returns a `Conversations` select

View File

@ -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

View File

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

View File

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

18
vendor/github.com/nlopes/slack/errors.go generated vendored Normal file
View File

@ -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")
)

View File

@ -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, &params, 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
}

9
vendor/github.com/nlopes/slack/go.mod generated vendored Normal file
View File

@ -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
)

22
vendor/github.com/nlopes/slack/go.sum generated vendored Normal file
View File

@ -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=

View File

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

20
vendor/github.com/nlopes/slack/im.go generated vendored
View File

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

View File

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

View File

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

View File

@ -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)
}

18
vendor/github.com/nlopes/slack/internal/timex/timex.go generated vendored Normal file
View File

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

View File

@ -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"`

View File

@ -8,6 +8,7 @@ import (
"fmt"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"net/http"
"net/http/httputil"
@ -48,34 +49,98 @@ 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)
req = req.WithContext(ctx)
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.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req, nil
}
func jsonReq(endpoint string, body interface{}) (req *http.Request, err error) {
buffer := bytes.NewBuffer([]byte{})
if err = json.NewEncoder(buffer).Encode(body); err != nil {
return nil, err
}
if req, err = http.NewRequest("POST", endpoint, buffer); err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
return req, nil
}
func parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error {
response, err := ioutil.ReadAll(body)
if err != 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}
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}
}
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)
}
}
}

View File

@ -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()

View File

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

View File

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

View File

@ -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()

View File

@ -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),

View File

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

View File

@ -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 {

View File

@ -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)
}

View File

@ -55,3 +55,8 @@ func EscapeMessage(message string) string {
replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
return replacer.Replace(message)
}
// Retryable errors return true.
type Retryable interface {
Retryable() bool
}

View File

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

View File

@ -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)
}

View File

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

View File

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

View File

@ -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{})
}

View File

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

View File

@ -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

View File

@ -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.
select {
case <-rtm.disconnected:
rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: true}}
return nil, nil, fmt.Errorf("disconnect received while trying to connect")
default:
}
// 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)
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:
return nil, nil, ErrRTMDisconnected
}
}
}
@ -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.isConnected = false
rtm.wasIntentional = intentional
err := rtm.conn.Close()
rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{intentional}}
rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: intentional, Cause: cause}}
if intentional {
rtm.disconnect()
}
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
}