Vendoring: Update nlopes/slack dep to master

Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>
This commit is contained in:
Prasad Ghangal 2019-08-06 14:37:34 +05:30
parent a0e49d5bc2
commit ec65f308d1
60 changed files with 2445 additions and 921 deletions

2
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/mattermost/mattermost-server v5.11.1+incompatible
github.com/mattn/go-sqlite3 v1.11.0 // indirect
github.com/nicksnyder/go-i18n v1.10.1 // indirect
github.com/nlopes/slack v0.4.0
github.com/nlopes/slack v0.5.1-0.20190623232825-2891986e2a3e
github.com/olivere/elastic v6.2.21+incompatible
github.com/pborman/uuid v1.2.0 // indirect
github.com/pkg/errors v0.8.1 // indirect

4
go.sum
View File

@ -68,6 +68,7 @@ github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsC
github.com/googleapis/gnostic v0.3.0 h1:CcQijm0XKekKjP/YCz28LXVSpgguuB+nCxaSjCe09y0=
github.com/googleapis/gnostic v0.3.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
@ -125,6 +126,8 @@ github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDa
github.com/nicksnyder/go-i18n v1.10.1/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4=
github.com/nlopes/slack v0.4.0 h1:OVnHm7lv5gGT5gkcHsZAyw++oHVFihbjWbL3UceUpiA=
github.com/nlopes/slack v0.4.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
github.com/nlopes/slack v0.5.1-0.20190623232825-2891986e2a3e h1:4X/3/ywN6f+XITaDoI9GjXbF7MHk7fuunXsEWZX1GLc=
github.com/nlopes/slack v0.5.1-0.20190623232825-2891986e2a3e/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk=
github.com/olivere/elastic v6.2.21+incompatible h1:QnTuofzxOCV5FrYLywjkMxOmOWhAeild1VXxKRksK9Y=
github.com/olivere/elastic v6.2.21+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -140,6 +143,7 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

14
vendor/github.com/nlopes/slack/.gometalinter.json generated vendored Normal file
View File

@ -0,0 +1,14 @@
{
"DisableAll": true,
"Enable": [
"structcheck",
"vet",
"misspell",
"unconvert",
"interfacer",
"goimports"
],
"Vendor": true,
"Exclude": ["vendor"],
"Deadline": "300s"
}

View File

@ -1,21 +1,35 @@
language: go
go:
- 1.7.x
- 1.8.x
- 1.9.x
- tip
env:
- GO111MODULE=on
install: true
before_install:
- export PATH=$HOME/gopath/bin:$PATH
# install gometalinter
- curl -L https://git.io/vp6lP | sh
script:
- go test -race ./...
- go test -cover ./...
- PATH=$PWD/bin:$PATH gometalinter ./...
- go test -race -cover ./...
matrix:
allow_failures:
- go: tip
allow_failures:
- go: tip
include:
- go: "1.7.x"
script: go test -v ./...
- go: "1.8.x"
script: go test -v ./...
- go: "1.9.x"
script: go test -v ./...
- go: "1.10.x"
script: go test -v ./...
- go: "1.11.x"
script: go test -v -mod=vendor ./...
- go: "tip"
script: go test -v -mod=vendor ./...
git:
depth: 10

View File

@ -1,3 +1,8 @@
### v0.5.0 - January 20, 2019
full differences can be viewed using `git log --oneline --decorate --color v0.4.0..v0.5.0`
- Breaking changes: various old struct fields have been removed or updated to match slack's api.
- deadlock fix in RTM disconnect.
### v0.4.0 - October 06, 2018
full differences can be viewed using `git log --oneline --decorate --color v0.3.0..v0.4.0`
- Breaking Change: renamed ApplyMessageOption, to mark it as unsafe,

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

@ -9,18 +9,10 @@ a fully managed way.
## Change log
Support for the EventsAPI has recently been added. It is still in its early stages but nearly all events have been added and tested (except for those events in [Developer Preview](https://api.slack.com/slack-apps-preview) mode). API stability for events is not promised at this time.
### v0.2.0 - Feb 10, 2018
## Changelog
Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against.
Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0)
### CHANGELOG.md
[CHANGELOG.md](https://github.com/nlopes/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates.
[CHANGELOG.md](https://github.com/nlopes/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates.
## Installing
@ -43,7 +35,7 @@ func main() {
api := slack.New("YOUR_TOKEN_HERE")
// If you set debugging, it will log all requests to the console
// Useful when encountering issues
// api.SetDebug(true)
// slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true))
groups, err := api.GetGroups(false)
if err != nil {
fmt.Printf("%s\n", err)

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 HTTPRequester, method string, teamName string, values url.Values, debug bool) (*adminResponse, error) {
adminResponse := &adminResponse{}
err := parseAdminResponse(ctx, client, method, teamName, values, adminResponse, debug)
func (api *Client) adminRequest(ctx context.Context, method string, teamName string, values url.Values) error {
resp := &SlackResponse{}
err := parseAdminResponse(ctx, api.httpclient, method, teamName, values, resp, api)
if err != nil {
return nil, err
return err
}
if !adminResponse.OK {
return nil, errors.New(adminResponse.Error)
}
return adminResponse, nil
return resp.Err()
}
// DisableUser disabled a user account, given a user ID
@ -40,9 +31,8 @@ func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "setInactive", teamName, values, api.debug)
if err != nil {
return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err)
if err := api.adminRequest(ctx, "setInactive", teamName, values); err != nil {
return fmt.Errorf("failed to disable user with id '%s': %s", uid, err)
}
return nil
@ -67,7 +57,7 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug)
err := api.adminRequest(ctx, "invite", teamName, values)
if err != nil {
return fmt.Errorf("Failed to invite single-channel guest: %s", err)
}
@ -94,7 +84,7 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug)
err := api.adminRequest(ctx, "invite", teamName, values)
if err != nil {
return fmt.Errorf("Failed to restricted account: %s", err)
}
@ -118,7 +108,7 @@ func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName,
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug)
err := api.adminRequest(ctx, "invite", teamName, values)
if err != nil {
return fmt.Errorf("Failed to invite to team: %s", err)
}
@ -140,7 +130,7 @@ func (api *Client) SetRegularContext(ctx context.Context, teamName, user string)
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "setRegular", teamName, values, api.debug)
err := api.adminRequest(ctx, "setRegular", teamName, values)
if err != nil {
return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err)
}
@ -162,7 +152,7 @@ func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, use
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "sendSSOBind", teamName, values, api.debug)
err := api.adminRequest(ctx, "sendSSOBind", teamName, values)
if err != nil {
return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err)
}
@ -185,7 +175,7 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid,
"_attempts": {"1"},
}
_, err := adminRequest(ctx, api.httpclient, "setUltraRestricted", teamName, values, api.debug)
err := api.adminRequest(ctx, "setUltraRestricted", teamName, values)
if err != nil {
return fmt.Errorf("Failed to ultra-restrict account: %s", err)
}
@ -194,22 +184,23 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid,
}
// SetRestricted converts a user into a restricted account
func (api *Client) SetRestricted(teamName, uid string) error {
return api.SetRestrictedContext(context.Background(), teamName, uid)
func (api *Client) SetRestricted(teamName, uid string, channelIds ...string) error {
return api.SetRestrictedContext(context.Background(), teamName, uid, channelIds...)
}
// SetRestrictedContext converts a user into a restricted account with a custom context
func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string) error {
func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string, channelIds ...string) error {
values := url.Values{
"user": {uid},
"token": {api.token},
"set_active": {"true"},
"_attempts": {"1"},
"channels": {strings.Join(channelIds, ",")},
}
_, err := adminRequest(ctx, api.httpclient, "setRestricted", teamName, values, api.debug)
err := api.adminRequest(ctx, "setRestricted", teamName, values)
if err != nil {
return fmt.Errorf("Failed to restrict account: %s", err)
return fmt.Errorf("failed to restrict account: %s", err)
}
return nil

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.
@ -42,25 +47,8 @@ type AttachmentActionOptionGroup struct {
}
// AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction)
type AttachmentActionCallback struct {
Actions []AttachmentAction `json:"actions"`
CallbackID string `json:"callback_id"`
Team Team `json:"team"`
Channel Channel `json:"channel"`
User User `json:"user"`
Name string `json:"name"`
Value string `json:"value"`
OriginalMessage Message `json:"original_message"`
ActionTs string `json:"action_ts"`
MessageTs string `json:"message_ts"`
AttachmentID string `json:"attachment_id"`
Token string `json:"token"`
ResponseURL string `json:"response_url"`
TriggerID string `json:"trigger_id"`
}
// DEPRECATED: use InteractionCallback
type AttachmentActionCallback InteractionCallback
// ConfirmationField are used to ask users to confirm actions
type ConfirmationField struct {

40
vendor/github.com/nlopes/slack/auth.go generated vendored Normal file
View File

@ -0,0 +1,40 @@
package slack
import (
"context"
"net/url"
)
// AuthRevokeResponse contains our Auth response from the auth.revoke endpoint
type AuthRevokeResponse struct {
SlackResponse // Contains the "ok", and "Error", if any
Revoked bool `json:"revoked,omitempty"`
}
// authRequest sends the actual request, and unmarshals the response
func (api *Client) authRequest(ctx context.Context, path string, values url.Values) (*AuthRevokeResponse, error) {
response := &AuthRevokeResponse{}
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
return response, response.Err()
}
// SendAuthRevoke will send a revocation for our token
func (api *Client) SendAuthRevoke(token string) (*AuthRevokeResponse, error) {
return api.SendAuthRevokeContext(context.Background(), token)
}
// SendAuthRevokeContext will retrieve the satus from api.test
func (api *Client) SendAuthRevokeContext(ctx context.Context, token string) (*AuthRevokeResponse, error) {
if token == "" {
token = api.token
}
values := url.Values{
"token": {token},
}
return api.authRequest(ctx, "auth.revoke", values)
}

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

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

@ -0,0 +1,215 @@
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"`
}
// 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 HTTPRequester, path string, values url.Values, debug bool) (*botResponseFull, error) {
func (api *Client) botRequest(ctx context.Context, path string, values url.Values) (*botResponseFull, error) {
response := &botResponseFull{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, err
}
return response, nil
}
@ -43,7 +44,7 @@ func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, err
"bot": {bot},
}
response, err := botRequest(ctx, api.httpclient, "bots.info", values, api.debug)
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,25 +18,50 @@ type channelResponseFull struct {
// Channel contains information about the channel
type Channel struct {
groupConversation
GroupConversation
IsChannel bool `json:"is_channel"`
IsGeneral bool `json:"is_general"`
IsMember bool `json:"is_member"`
Locale string `json:"locale"`
}
func channelRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*channelResponseFull, error) {
func (api *Client) channelRequest(ctx context.Context, path string, values url.Values) (*channelResponseFull, error) {
response := &channelResponseFull{}
err := postForm(ctx, client, SLACK_API+path, values, response, debug)
err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, err
}
return response, nil
}
type channelsConfig struct {
values url.Values
}
// GetChannelsOption option provided when getting channels.
type GetChannelsOption func(*channelsConfig) error
// GetChannelsOptionExcludeMembers excludes the members collection from each channel.
func GetChannelsOptionExcludeMembers() GetChannelsOption {
return func(config *channelsConfig) error {
config.values.Add("exclude_members", "true")
return nil
}
}
// GetChannelsOptionExcludeArchived excludes archived channels from results.
func GetChannelsOptionExcludeArchived() GetChannelsOption {
return func(config *channelsConfig) error {
config.values.Add("exclude_archived", "true")
return nil
}
}
// ArchiveChannel archives the given channel
// see https://api.slack.com/methods/channels.archive
func (api *Client) ArchiveChannel(channelID string) error {
@ -52,7 +76,7 @@ func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string)
"channel": {channelID},
}
_, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug)
_, err = api.channelRequest(ctx, "channels.archive", values)
return err
}
@ -70,7 +94,7 @@ func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string
"channel": {channelID},
}
_, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug)
_, err = api.channelRequest(ctx, "channels.unarchive", values)
return err
}
@ -88,7 +112,7 @@ func (api *Client) CreateChannelContext(ctx context.Context, channelName string)
"name": {channelName},
}
response, err := channelRequest(ctx, api.httpclient, "channels.create", values, api.debug)
response, err := api.channelRequest(ctx, "channels.create", values)
if err != nil {
return nil, err
}
@ -133,7 +157,7 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID strin
}
}
response, err := channelRequest(ctx, api.httpclient, "channels.history", values, api.debug)
response, err := api.channelRequest(ctx, "channels.history", values)
if err != nil {
return nil, err
}
@ -150,11 +174,12 @@ func (api *Client) GetChannelInfo(channelID string) (*Channel, error) {
// see https://api.slack.com/methods/channels.info
func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) {
values := url.Values{
"token": {api.token},
"channel": {channelID},
"token": {api.token},
"channel": {channelID},
"include_locale": {strconv.FormatBool(true)},
}
response, err := channelRequest(ctx, api.httpclient, "channels.info", values, api.debug)
response, err := api.channelRequest(ctx, "channels.info", values)
if err != nil {
return nil, err
}
@ -167,7 +192,7 @@ func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error)
return api.InviteUserToChannelContext(context.Background(), channelID, user)
}
// InviteUserToChannelCustom invites a user to a given channel and returns a *Channel with a custom context
// InviteUserToChannelContext invites a user to a given channel and returns a *Channel with a custom context
// see https://api.slack.com/methods/channels.invite
func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) {
values := url.Values{
@ -176,7 +201,7 @@ func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, us
"user": {user},
}
response, err := channelRequest(ctx, api.httpclient, "channels.invite", values, api.debug)
response, err := api.channelRequest(ctx, "channels.invite", values)
if err != nil {
return nil, err
}
@ -197,7 +222,7 @@ func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (
"name": {channelName},
}
response, err := channelRequest(ctx, api.httpclient, "channels.join", values, api.debug)
response, err := api.channelRequest(ctx, "channels.join", values)
if err != nil {
return nil, err
}
@ -218,7 +243,7 @@ func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (b
"channel": {channelID},
}
response, err := channelRequest(ctx, api.httpclient, "channels.leave", values, api.debug)
response, err := api.channelRequest(ctx, "channels.leave", values)
if err != nil {
return false, err
}
@ -241,27 +266,35 @@ func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, us
"user": {user},
}
_, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug)
_, err = api.channelRequest(ctx, "channels.kick", values)
return err
}
// GetChannels retrieves all the channels
// see https://api.slack.com/methods/channels.list
func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) {
return api.GetChannelsContext(context.Background(), excludeArchived)
func (api *Client) GetChannels(excludeArchived bool, options ...GetChannelsOption) ([]Channel, error) {
return api.GetChannelsContext(context.Background(), excludeArchived, options...)
}
// GetChannelsContext retrieves all the channels with a custom context
// see https://api.slack.com/methods/channels.list
func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) ([]Channel, error) {
values := url.Values{
"token": {api.token},
func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool, options ...GetChannelsOption) ([]Channel, error) {
config := channelsConfig{
values: url.Values{
"token": {api.token},
},
}
if excludeArchived {
values.Add("exclude_archived", "1")
options = append(options, GetChannelsOptionExcludeArchived())
}
response, err := channelRequest(ctx, api.httpclient, "channels.list", values, api.debug)
for _, opt := range options {
if err := opt(&config); err != nil {
return nil, err
}
}
response, err := api.channelRequest(ctx, "channels.list", config.values)
if err != nil {
return nil, err
}
@ -288,7 +321,7 @@ func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts
"ts": {ts},
}
_, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug)
_, err = api.channelRequest(ctx, "channels.mark", values)
return err
}
@ -309,7 +342,7 @@ func (api *Client) RenameChannelContext(ctx context.Context, channelID, name str
// XXX: the created entry in this call returns a string instead of a number
// so I may have to do some workaround to solve it.
response, err := channelRequest(ctx, api.httpclient, "channels.rename", values, api.debug)
response, err := api.channelRequest(ctx, "channels.rename", values)
if err != nil {
return nil, err
}
@ -331,7 +364,7 @@ func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purp
"purpose": {purpose},
}
response, err := channelRequest(ctx, api.httpclient, "channels.setPurpose", values, api.debug)
response, err := api.channelRequest(ctx, "channels.setPurpose", values)
if err != nil {
return "", err
}
@ -353,7 +386,7 @@ func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic
"topic": {topic},
}
response, err := channelRequest(ctx, api.httpclient, "channels.setTopic", values, api.debug)
response, err := api.channelRequest(ctx, "channels.setTopic", values)
if err != nil {
return "", err
}
@ -374,7 +407,7 @@ func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thre
"channel": {channelID},
"thread_ts": {thread_ts},
}
response, err := channelRequest(ctx, api.httpclient, "channels.replies", values, api.debug)
response, err := api.channelRequest(ctx, "channels.replies", values)
if err != nil {
return nil, err
}

View File

@ -43,19 +43,18 @@ func (c chatResponseFull) getMessageTimestamp() string {
// PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request
type PostMessageParameters struct {
Username string `json:"username"`
AsUser bool `json:"as_user"`
Parse string `json:"parse"`
ThreadTimestamp string `json:"thread_ts"`
ReplyBroadcast bool `json:"reply_broadcast"`
LinkNames int `json:"link_names"`
Attachments []Attachment `json:"attachments"`
UnfurlLinks bool `json:"unfurl_links"`
UnfurlMedia bool `json:"unfurl_media"`
IconURL string `json:"icon_url"`
IconEmoji string `json:"icon_emoji"`
Markdown bool `json:"mrkdwn,omitempty"`
EscapeText bool `json:"escape_text"`
Username string `json:"username"`
AsUser bool `json:"as_user"`
Parse string `json:"parse"`
ThreadTimestamp string `json:"thread_ts"`
ReplyBroadcast bool `json:"reply_broadcast"`
LinkNames int `json:"link_names"`
UnfurlLinks bool `json:"unfurl_links"`
UnfurlMedia bool `json:"unfurl_media"`
IconURL string `json:"icon_url"`
IconEmoji string `json:"icon_emoji"`
Markdown bool `json:"mrkdwn,omitempty"`
EscapeText bool `json:"escape_text"`
// chat.postEphemeral support
Channel string `json:"channel"`
@ -71,7 +70,6 @@ func NewPostMessageParameters() PostMessageParameters {
Parse: DEFAULT_MESSAGE_PARSE,
ThreadTimestamp: DEFAULT_MESSAGE_THREAD_TIMESTAMP,
LinkNames: DEFAULT_MESSAGE_LINK_NAMES,
Attachments: nil,
UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS,
UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA,
IconURL: DEFAULT_MESSAGE_ICON_URL,
@ -96,26 +94,24 @@ func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTim
// PostMessage sends a message to a channel.
// Message is escaped by default according to https://api.slack.com/docs/formatting
// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message.
func (api *Client) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) {
func (api *Client) PostMessage(channelID string, options ...MsgOption) (string, string, error) {
respChannel, respTimestamp, _, err := api.SendMessageContext(
context.Background(),
channel,
MsgOptionText(text, params.EscapeText),
MsgOptionAttachments(params.Attachments...),
MsgOptionPostMessageParameters(params),
channelID,
MsgOptionPost(),
MsgOptionCompose(options...),
)
return respChannel, respTimestamp, err
}
// PostMessageContext sends a message to a channel with a custom context
// For more details, see PostMessage documentation
func (api *Client) PostMessageContext(ctx context.Context, channel, text string, params PostMessageParameters) (string, string, error) {
// For more details, see PostMessage documentation.
func (api *Client) PostMessageContext(ctx context.Context, channelID string, options ...MsgOption) (string, string, error) {
respChannel, respTimestamp, _, err := api.SendMessageContext(
ctx,
channel,
MsgOptionText(text, params.EscapeText),
MsgOptionAttachments(params.Attachments...),
MsgOptionPostMessageParameters(params),
channelID,
MsgOptionPost(),
MsgOptionCompose(options...),
)
return respChannel, respTimestamp, err
}
@ -135,18 +131,23 @@ func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption)
// PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context
// For more details, see PostEphemeral documentation
func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID string, options ...MsgOption) (timestamp string, err error) {
_, timestamp, _, err = api.SendMessageContext(ctx, channelID, append(options, MsgOptionPostEphemeral2(userID))...)
_, timestamp, _, err = api.SendMessageContext(ctx, channelID, MsgOptionPostEphemeral(userID), MsgOptionCompose(options...))
return timestamp, err
}
// UpdateMessage updates a message in a channel
func (api *Client) UpdateMessage(channelID, timestamp, text string) (string, string, string, error) {
return api.UpdateMessageContext(context.Background(), channelID, timestamp, text)
func (api *Client) UpdateMessage(channelID, timestamp string, options ...MsgOption) (string, string, string, error) {
return api.SendMessageContext(context.Background(), channelID, MsgOptionUpdate(timestamp), MsgOptionCompose(options...))
}
// UpdateMessageContext updates a message in a channel
func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp, text string) (string, string, string, error) {
return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionText(text, true))
func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, string, error) {
return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionCompose(options...))
}
// UnfurlMessage unfurls a message in a channel
func (api *Client) UnfurlMessage(channelID, timestamp string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) {
return api.SendMessageContext(context.Background(), channelID, MsgOptionUnfurl(timestamp, unfurls), MsgOptionCompose(options...))
}
// SendMessage more flexible method for configuring messages.
@ -161,11 +162,11 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt
response chatResponseFull
)
if config, err = applyMsgOptions(api.token, channelID, options...); err != nil {
if config, err = applyMsgOptions(api.token, channelID, api.endpoint, options...); err != nil {
return "", "", "", err
}
if err = postForm(ctx, api.httpclient, config.endpoint, config.values, &response, api.debug); err != nil {
if err = postForm(ctx, api.httpclient, config.endpoint, config.values, &response, api); err != nil {
return "", "", "", err
}
@ -175,14 +176,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: SLACK_API + string(chatPostMessage),
apiurl: apiurl,
endpoint: apiurl + string(chatPostMessage),
values: url.Values{
"token": {token},
"channel": {channel},
@ -206,9 +208,11 @@ const (
chatDelete sendMode = "chat.delete"
chatPostEphemeral sendMode = "chat.postEphemeral"
chatMeMessage sendMode = "chat.meMessage"
chatUnfurl sendMode = "chat.unfurl"
)
type sendConfig struct {
apiurl string
endpoint string
values url.Values
}
@ -219,26 +223,16 @@ type MsgOption func(*sendConfig) error
// MsgOptionPost posts a messages, this is the default.
func MsgOptionPost() MsgOption {
return func(config *sendConfig) error {
config.endpoint = SLACK_API + string(chatPostMessage)
config.endpoint = config.apiurl + string(chatPostMessage)
config.values.Del("ts")
return nil
}
}
// MsgOptionPostEphemeral - DEPRECATED: use MsgOptionPostEphemeral2
// posts an ephemeral message.
func MsgOptionPostEphemeral() MsgOption {
// MsgOptionPostEphemeral - posts an ephemeral message to the provided user.
func MsgOptionPostEphemeral(userID string) MsgOption {
return func(config *sendConfig) error {
config.endpoint = SLACK_API + string(chatPostEphemeral)
config.values.Del("ts")
return nil
}
}
// MsgOptionPostEphemeral2 - posts an ephemeral message to the provided user.
func MsgOptionPostEphemeral2(userID string) MsgOption {
return func(config *sendConfig) error {
config.endpoint = SLACK_API + string(chatPostEphemeral)
config.endpoint = config.apiurl + string(chatPostEphemeral)
MsgOptionUser(userID)(config)
config.values.Del("ts")
@ -249,7 +243,7 @@ func MsgOptionPostEphemeral2(userID string) MsgOption {
// MsgOptionMeMessage posts a "me message" type from the calling user
func MsgOptionMeMessage() MsgOption {
return func(config *sendConfig) error {
config.endpoint = SLACK_API + string(chatMeMessage)
config.endpoint = config.apiurl + string(chatMeMessage)
return nil
}
}
@ -257,7 +251,7 @@ func MsgOptionMeMessage() MsgOption {
// MsgOptionUpdate updates a message based on the timestamp.
func MsgOptionUpdate(timestamp string) MsgOption {
return func(config *sendConfig) error {
config.endpoint = SLACK_API + string(chatUpdate)
config.endpoint = config.apiurl + string(chatUpdate)
config.values.Add("ts", timestamp)
return nil
}
@ -266,12 +260,25 @@ 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 = SLACK_API + string(chatDelete)
config.endpoint = config.apiurl + string(chatDelete)
config.values.Add("ts", timestamp)
return nil
}
}
// MsgOptionUnfurl unfurls a message based on the timestamp.
func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption {
return func(config *sendConfig) error {
config.endpoint = config.apiurl + string(chatUnfurl)
config.values.Add("ts", timestamp)
unfurlsStr, err := json.Marshal(unfurls)
if err == nil {
config.values.Add("unfurls", string(unfurlsStr))
}
return err
}
}
// MsgOptionAsUser whether or not to send the message as the user.
func MsgOptionAsUser(b bool) MsgOption {
return func(config *sendConfig) error {
@ -290,6 +297,14 @@ func MsgOptionUser(userID string) MsgOption {
}
}
// MsgOptionUsername set the username for the message.
func MsgOptionUsername(username string) MsgOption {
return func(config *sendConfig) error {
config.values.Set("username", username)
return nil
}
}
// MsgOptionText provide the text for the message, optionally escape the provided
// text.
func MsgOptionText(text string, escape bool) MsgOption {
@ -317,6 +332,21 @@ func MsgOptionAttachments(attachments ...Attachment) MsgOption {
}
}
// 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
}
}
// MsgOptionEnableLinkUnfurl enables link unfurling
func MsgOptionEnableLinkUnfurl() MsgOption {
return func(config *sendConfig) error {
@ -365,7 +395,7 @@ func MsgOptionBroadcast() MsgOption {
}
}
// this function combines multiple options into a single option.
// MsgOptionCompose combines multiple options into a single option.
func MsgOptionCompose(options ...MsgOption) MsgOption {
return func(c *sendConfig) error {
for _, opt := range options {
@ -377,19 +407,36 @@ func MsgOptionCompose(options ...MsgOption) MsgOption {
}
}
// MsgOptionParse set parse option.
func MsgOptionParse(b bool) MsgOption {
return func(c *sendConfig) error {
var v string
if b {
v = "1"
v = "full"
} else {
v = "0"
v = "none"
}
c.values.Set("parse", v)
return nil
}
}
// MsgOptionIconURL sets an icon URL
func MsgOptionIconURL(iconURL string) MsgOption {
return func(c *sendConfig) error {
c.values.Set("icon_url", iconURL)
return nil
}
}
// MsgOptionIconEmoji sets an icon emoji
func MsgOptionIconEmoji(iconEmoji string) MsgOption {
return func(c *sendConfig) error {
c.values.Set("icon_emoji", iconEmoji)
return nil
}
}
// UnsafeMsgOptionEndpoint deliver the message to the specified endpoint.
// NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this Option
// will be supported by the library, it is subject to change without notice that
@ -456,3 +503,38 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
return nil
}
}
// PermalinkParameters are the parameters required to get a permalink to a
// message. Slack documentation can be found here:
// https://api.slack.com/methods/chat.getPermalink
type PermalinkParameters struct {
Channel string
Ts string
}
// GetPermalink returns the permalink for a message. It takes
// PermalinkParameters and returns a string containing the permalink. It
// returns an error if unable to retrieve the permalink.
func (api *Client) GetPermalink(params *PermalinkParameters) (string, error) {
return api.GetPermalinkContext(context.Background(), params)
}
// GetPermalinkContext returns the permalink for a message using a custom context.
func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkParameters) (string, error) {
values := url.Values{
"token": {api.token},
"channel": {params.Channel},
"message_ts": {params.Ts},
}
response := struct {
Channel string `json:"channel"`
Permalink string `json:"permalink"`
SlackResponse
}{}
err := api.getMethod(ctx, "chat.getPermalink", values, &response)
if err != nil {
return "", err
}
return response.Permalink, response.Err()
}

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,15 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge
ResponseMetaData responseMetaData `json:"response_metadata"`
SlackResponse
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.members", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.members", values, &response)
if err != nil {
return nil, "", err
}
if !response.Ok {
return nil, "", errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, "", err
}
return response.Members, response.ResponseMetaData.NextCursor, nil
}
@ -118,7 +120,9 @@ func (api *Client) GetConversationsForUser(params *GetConversationsForUserParame
func (api *Client) GetConversationsForUserContext(ctx context.Context, params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) {
values := url.Values{
"token": {api.token},
"user": {params.UserID},
}
if params.UserID != "" {
values.Add("user", params.UserID)
}
if params.Cursor != "" {
values.Add("cursor", params.Cursor)
@ -129,19 +133,20 @@ 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.debug)
err = api.postMethod(ctx, "users.conversations", values, &response)
if err != nil {
return nil, "", err
}
if !response.Ok {
return nil, "", errors.New(response.Error)
}
return response.Channels, response.ResponseMetaData.NextCursor, nil
return response.Channels, response.ResponseMetaData.NextCursor, response.Err()
}
// ArchiveConversation archives a conversation
@ -156,7 +161,7 @@ func (api *Client) ArchiveConversationContext(ctx context.Context, channelID str
"channel": {channelID},
}
response := SlackResponse{}
err := postSlackMethod(ctx, api.httpclient, "conversations.archive", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.archive", values, &response)
if err != nil {
return err
}
@ -176,7 +181,7 @@ func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID s
"channel": {channelID},
}
response := SlackResponse{}
err := postSlackMethod(ctx, api.httpclient, "conversations.unarchive", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.unarchive", values, &response)
if err != nil {
return err
}
@ -200,7 +205,7 @@ func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID,
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.setTopic", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.setTopic", values, &response)
if err != nil {
return nil, err
}
@ -224,7 +229,7 @@ func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelI
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.setPurpose", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.setPurpose", values, &response)
if err != nil {
return nil, err
}
@ -248,7 +253,7 @@ func (api *Client) RenameConversationContext(ctx context.Context, channelID, cha
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.rename", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.rename", values, &response)
if err != nil {
return nil, err
}
@ -272,7 +277,7 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.invite", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.invite", values, &response)
if err != nil {
return nil, err
}
@ -293,7 +298,7 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI
"user": {user},
}
response := SlackResponse{}
err := postSlackMethod(ctx, api.httpclient, "conversations.kick", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.kick", values, &response)
if err != nil {
return err
}
@ -318,7 +323,7 @@ func (api *Client) CloseConversationContext(ctx context.Context, channelID strin
AlreadyClosed bool `json:"already_closed"`
}{}
err = postSlackMethod(ctx, api.httpclient, "conversations.close", values, &response, api.debug)
err = api.postMethod(ctx, "conversations.close", values, &response)
if err != nil {
return false, false, err
}
@ -338,13 +343,12 @@ func (api *Client) CreateConversationContext(ctx context.Context, channelName st
"name": {channelName},
"is_private": {strconv.FormatBool(isPrivate)},
}
response, err := channelRequest(
ctx, api.httpclient, "conversations.create", values, api.debug)
response, err := api.channelRequest(ctx, "conversations.create", values)
if err != nil {
return nil, err
}
return &response.Channel, response.Err()
return &response.Channel, nil
}
// GetConversationInfo retrieves information about a conversation
@ -359,8 +363,7 @@ func (api *Client) GetConversationInfoContext(ctx context.Context, channelID str
"channel": {channelID},
"include_locale": {strconv.FormatBool(includeLocale)},
}
response, err := channelRequest(
ctx, api.httpclient, "conversations.info", values, api.debug)
response, err := api.channelRequest(ctx, "conversations.info", values)
if err != nil {
return nil, err
}
@ -380,7 +383,7 @@ func (api *Client) LeaveConversationContext(ctx context.Context, channelID strin
"channel": {channelID},
}
response, err := channelRequest(ctx, api.httpclient, "conversations.leave", values, api.debug)
response, err := api.channelRequest(ctx, "conversations.leave", values)
if err != nil {
return false, err
}
@ -436,7 +439,7 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge
Messages []Message `json:"messages"`
}{}
err = postSlackMethod(ctx, api.httpclient, "conversations.replies", values, &response, api.debug)
err = api.postMethod(ctx, "conversations.replies", values, &response)
if err != nil {
return nil, false, "", err
}
@ -476,7 +479,7 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve
ResponseMetaData responseMetaData `json:"response_metadata"`
SlackResponse
}{}
err = postSlackMethod(ctx, api.httpclient, "conversations.list", values, &response, api.debug)
err = api.postMethod(ctx, "conversations.list", values, &response)
if err != nil {
return nil, "", err
}
@ -513,7 +516,7 @@ func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConv
AlreadyOpen bool `json:"already_open"`
SlackResponse
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.open", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.open", values, &response)
if err != nil {
return nil, false, false, err
}
@ -537,7 +540,7 @@ func (api *Client) JoinConversationContext(ctx context.Context, channelID string
} `json:"response_metadata"`
SlackResponse
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.join", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.join", values, &response)
if err != nil {
return nil, "", nil, err
}
@ -599,12 +602,10 @@ func (api *Client) GetConversationHistoryContext(ctx context.Context, params *Ge
response := GetConversationHistoryResponse{}
err := postSlackMethod(ctx, api.httpclient, "conversations.history", values, &response, api.debug)
err := api.postMethod(ctx, "conversations.history", values, &response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return &response, nil
return &response, response.Err()
}

View File

@ -3,7 +3,7 @@ package slack
import (
"context"
"encoding/json"
"errors"
"strings"
)
// InputType is the type of the dialog input type
@ -14,7 +14,7 @@ const (
InputTypeText InputType = "text"
// InputTypeTextArea textarea input
InputTypeTextArea InputType = "textarea"
// InputTypeSelect textfield input
// InputTypeSelect select menus input
InputTypeSelect InputType = "select"
)
@ -25,6 +25,7 @@ type DialogInput struct {
Name string `json:"name"`
Placeholder string `json:"placeholder"`
Optional bool `json:"optional"`
Hint string `json:"hint"`
}
// DialogTrigger ...
@ -36,8 +37,9 @@ type DialogTrigger struct {
// Dialog as in Slack dialogs
// https://api.slack.com/dialogs#option_element_attributes#top-level_dialog_attributes
type Dialog struct {
TriggerID string `json:"trigger_id"` //Required
CallbackID string `json:"callback_id"` //Required
TriggerID string `json:"trigger_id"` // Required
CallbackID string `json:"callback_id"` // Required
State string `json:"state,omitempty"` // Optional
Title string `json:"title"`
SubmitLabel string `json:"submit_label,omitempty"`
NotifyOnCancel bool `json:"notify_on_cancel"`
@ -47,30 +49,13 @@ type Dialog struct {
// DialogElement abstract type for dialogs.
type DialogElement interface{}
// DialogCallback is sent from Slack when a user submits a form from within a dialog
type DialogCallback struct {
Type string `json:"type"`
CallbackID string `json:"callback_id"`
Team Team `json:"team"`
Channel Channel `json:"channel"`
User User `json:"user"`
ActionTs string `json:"action_ts"`
Token string `json:"token"`
ResponseURL string `json:"response_url"`
Submission map[string]string `json:"submission"`
}
// DialogCallback DEPRECATED use InteractionCallback
type DialogCallback InteractionCallback
// DialogSuggestionCallback is sent from Slack when a user types in a select field with an external data source
type DialogSuggestionCallback struct {
Type string `json:"type"`
Token string `json:"token"`
ActionTs string `json:"action_ts"`
Team Team `json:"team"`
User User `json:"user"`
Channel Channel `json:"channel"`
ElementName string `json:"name"`
Value string `json:"value"`
CallbackID string `json:"callback_id"`
// DialogSubmissionCallback is sent from Slack when a user submits a form from within a dialog
type DialogSubmissionCallback struct {
State string `json:"state,omitempty"`
Submission map[string]string `json:"submission"`
}
// DialogOpenResponse response from `dialog.open`
@ -84,6 +69,17 @@ type DialogResponseMetadata struct {
Messages []string `json:"messages"`
}
// DialogInputValidationError is an error when user inputs incorrect value to form from within a dialog
type DialogInputValidationError struct {
Name string `json:"name"`
Error string `json:"error"`
}
// DialogInputValidationErrors lists the name of field and that error messages
type DialogInputValidationErrors struct {
Errors []DialogInputValidationError `json:"errors"`
}
// OpenDialog opens a dialog window where the triggerID originated from.
// EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable.
func (api *Client) OpenDialog(triggerID string, dialog Dialog) (err error) {
@ -94,7 +90,7 @@ func (api *Client) OpenDialog(triggerID string, dialog Dialog) (err error) {
// EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable.
func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dialog Dialog) (err error) {
if triggerID == "" {
return errors.New("received empty parameters")
return ErrParametersMissing
}
req := DialogTrigger{
@ -108,10 +104,15 @@ func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dial
}
response := &DialogOpenResponse{}
endpoint := SLACK_API + "dialog.open"
if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api.debug); err != nil {
endpoint := api.endpoint + "dialog.open"
if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api); err != nil {
return err
}
if len(response.DialogResponseMetadata.Messages) > 0 {
response.Ok = false
response.Error += "\n" + strings.Join(response.DialogResponseMetadata.Messages, "\n")
}
return response.Err()
}

View File

@ -24,6 +24,8 @@ type DialogInputSelect struct {
SelectedOptions string `json:"selected_options,omitempty"` //Optional. Default value for "external" only
Options []DialogSelectOption `json:"options,omitempty"` //One of options or option_groups is required.
OptionGroups []DialogOptionGroup `json:"option_groups,omitempty"` //Provide up to 100 options.
MinQueryLength int `json:"min_query_length,omitempty"` //Optional. minimum characters before query is sent.
Hint string `json:"hint,omitempty"` //Optional. Additional hint text.
}
// DialogSelectOption is an option for the user to select from the menu
@ -53,14 +55,7 @@ func NewStaticSelectDialogInput(name, label string, options []DialogSelectOption
}
// NewGroupedSelectDialogInput creates grouped options select input for Dialogs.
func NewGroupedSelectDialogInput(name, label string, groups map[string]map[string]string) *DialogInputSelect {
optionGroups := []DialogOptionGroup{}
for groupName, options := range groups {
optionGroups = append(optionGroups, DialogOptionGroup{
Label: groupName,
Options: optionsFromMap(options),
})
}
func NewGroupedSelectDialogInput(name, label string, options []DialogOptionGroup) *DialogInputSelect {
return &DialogInputSelect{
DialogInput: DialogInput{
Type: InputTypeSelect,
@ -68,34 +63,15 @@ func NewGroupedSelectDialogInput(name, label string, groups map[string]map[strin
Label: label,
},
DataSource: DialogDataSourceStatic,
OptionGroups: optionGroups,
}
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

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
"strings"
@ -36,16 +35,14 @@ type dndTeamInfoResponse struct {
SlackResponse
}
func dndRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*dndResponseFull, error) {
func (api *Client) dndRequest(ctx context.Context, path string, values url.Values) (*dndResponseFull, error) {
response := &dndResponseFull{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// EndDND ends the user's scheduled Do Not Disturb session
@ -61,7 +58,7 @@ func (api *Client) EndDNDContext(ctx context.Context) error {
response := &SlackResponse{}
if err := postSlackMethod(ctx, api.httpclient, "dnd.endDnd", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "dnd.endDnd", values, response); err != nil {
return err
}
@ -79,7 +76,7 @@ func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) {
"token": {api.token},
}
response, err := dndRequest(ctx, api.httpclient, "dnd.endSnooze", values, api.debug)
response, err := api.dndRequest(ctx, "dnd.endSnooze", values)
if err != nil {
return nil, err
}
@ -100,7 +97,7 @@ func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDSta
values.Set("user", *user)
}
response, err := dndRequest(ctx, api.httpclient, "dnd.info", values, api.debug)
response, err := api.dndRequest(ctx, "dnd.info", values)
if err != nil {
return nil, err
}
@ -120,12 +117,14 @@ func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (m
}
response := &dndTeamInfoResponse{}
if err := postSlackMethod(ctx, api.httpclient, "dnd.teamInfo", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "dnd.teamInfo", values, response); err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
if response.Err() != nil {
return nil, response.Err()
}
return response.Users, nil
}
@ -136,7 +135,7 @@ func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) {
return api.SetSnoozeContext(context.Background(), minutes)
}
// SetSnooze adjusts the snooze duration for a user's Do Not Disturb settings with a custom context.
// SetSnoozeContext adjusts the snooze duration for a user's Do Not Disturb settings with a custom context.
// For more information see the SetSnooze docs
func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) {
values := url.Values{
@ -144,7 +143,7 @@ func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatu
"num_minutes": {strconv.Itoa(minutes)},
}
response, err := dndRequest(ctx, api.httpclient, "dnd.setSnooze", values, api.debug)
response, err := api.dndRequest(ctx, "dnd.setSnooze", values)
if err != nil {
return nil, err
}

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.debug)
err := api.postMethod(ctx, "emoji.list", values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
if response.Err() != nil {
return nil, response.Err()
}
return response.Emoji, nil
}

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,7 @@ package slack
import (
"context"
"errors"
"fmt"
"io"
"net/url"
"strconv"
@ -86,12 +86,30 @@ type File struct {
CommentsCount int `json:"comments_count"`
NumStars int `json:"num_stars"`
IsStarred bool `json:"is_starred"`
Shares Share `json:"shares"`
}
type Share struct {
Public map[string][]ShareFileInfo `json:"public"`
}
type ShareFileInfo struct {
ReplyUsers []string `json:"reply_users"`
ReplyUsersCount int `json:"reply_users_count"`
ReplyCount int `json:"reply_count"`
Ts string `json:"ts"`
ThreadTs string `json:"thread_ts"`
LatestReply string `json:"latest_reply"`
ChannelName string `json:"channel_name"`
TeamID string `json:"team_id"`
}
// FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request.
//
// There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large,
// or provide a local file path in File to upload it from your filesystem.
//
// Note that when using the Reader option, you *must* specify the Filename, otherwise the Slack API isn't happy.
type FileUploadParameters struct {
File string
Content string
@ -115,11 +133,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
}
@ -137,16 +165,14 @@ func NewGetFilesParameters() GetFilesParameters {
}
}
func fileRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*fileResponseFull, error) {
func (api *Client) fileRequest(ctx context.Context, path string, values url.Values) (*fileResponseFull, error) {
response := &fileResponseFull{}
err := postForm(ctx, client, SLACK_API+path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// GetFileInfo retrieves a file and related comments
@ -163,18 +189,57 @@ func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count,
"page": {strconv.Itoa(page)},
}
response, err := fileRequest(ctx, api.httpclient, "files.info", values, api.debug)
response, err := api.fileRequest(ctx, "files.info", values)
if err != nil {
return nil, nil, nil, err
}
return &response.File, response.Comments, &response.Paging, nil
}
// GetFile retreives a given file from its private download URL
func (api *Client) GetFile(downloadURL string, writer io.Writer) error {
return downloadFile(api.httpclient, api.token, downloadURL, writer, api)
}
// GetFiles retrieves all files according to the parameters given
func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) {
return api.GetFilesContext(context.Background(), params)
}
// ListFiles retrieves all files according to the parameters given. Uses cursor based pagination.
func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) {
return api.ListFilesContext(context.Background(), params)
}
// ListFilesContext retrieves all files according to the parameters given with a custom context. Uses cursor based pagination.
func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) {
values := url.Values{
"token": {api.token},
}
if params.User != DEFAULT_FILES_USER {
values.Add("user", params.User)
}
if params.Channel != DEFAULT_FILES_CHANNEL {
values.Add("channel", params.Channel)
}
if params.Limit != DEFAULT_FILES_COUNT {
values.Add("limit", strconv.Itoa(params.Limit))
}
if params.Cursor != "" {
values.Add("cursor", params.Cursor)
}
response, err := api.fileRequest(ctx, "files.list", values)
if err != nil {
return nil, nil, err
}
params.Cursor = response.Metadata.Cursor
return response.Files, &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{
@ -202,7 +267,7 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter
values.Add("page", strconv.Itoa(params.Page))
}
response, err := fileRequest(ctx, api.httpclient, "files.list", values, api.debug)
response, err := api.fileRequest(ctx, "files.list", values)
if err != nil {
return nil, nil, err
}
@ -222,6 +287,9 @@ 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},
@ -246,19 +314,17 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam
}
if params.Content != "" {
values.Add("content", params.Content)
err = postForm(ctx, api.httpclient, SLACK_API+"files.upload", values, response, api.debug)
err = api.postMethod(ctx, "files.upload", values, response)
} else if params.File != "" {
err = postLocalWithMultipartResponse(ctx, api.httpclient, "files.upload", params.File, "file", values, response, api.debug)
err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", values, response, api)
} else if params.Reader != nil {
err = postWithMultipartResponse(ctx, api.httpclient, "files.upload", params.Filename, "file", values, params.Reader, response, api.debug)
err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", values, params.Reader, response, api)
}
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return &response.File, nil
return &response.File, response.Err()
}
// DeleteFileComment deletes a file's comment
@ -269,7 +335,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{
@ -277,7 +343,7 @@ func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, comment
"file": {fileID},
"id": {commentID},
}
_, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug)
_, err = api.fileRequest(ctx, "files.comments.delete", values)
return err
}
@ -293,7 +359,7 @@ func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err er
"file": {fileID},
}
_, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug)
_, err = api.fileRequest(ctx, "files.delete", values)
return err
}
@ -309,7 +375,7 @@ func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string
"file": {fileID},
}
response, err := fileRequest(ctx, api.httpclient, "files.revokePublicURL", values, api.debug)
response, err := api.fileRequest(ctx, "files.revokePublicURL", values)
if err != nil {
return nil, err
}
@ -328,7 +394,7 @@ func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string)
"file": {fileID},
}
response, err := fileRequest(ctx, api.httpclient, "files.sharedPublicURL", values, api.debug)
response, err := api.fileRequest(ctx, "files.sharedPublicURL", values)
if err != nil {
return nil, nil, nil, err
}

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

@ -2,14 +2,13 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
// Group contains all the information for a group
type Group struct {
groupConversation
GroupConversation
IsGroup bool `json:"is_group"`
}
@ -28,16 +27,14 @@ type groupResponseFull struct {
SlackResponse
}
func groupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*groupResponseFull, error) {
func (api *Client) groupRequest(ctx context.Context, path string, values url.Values) (*groupResponseFull, error) {
response := &groupResponseFull{}
err := postForm(ctx, client, SLACK_API+path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// ArchiveGroup archives a private group
@ -52,7 +49,7 @@ func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error
"channel": {group},
}
_, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api.debug)
_, err := api.groupRequest(ctx, "groups.archive", values)
return err
}
@ -68,7 +65,7 @@ func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) erro
"channel": {group},
}
_, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api.debug)
_, err := api.groupRequest(ctx, "groups.unarchive", values)
return err
}
@ -84,7 +81,7 @@ func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group
"name": {group},
}
response, err := groupRequest(ctx, api.httpclient, "groups.create", values, api.debug)
response, err := api.groupRequest(ctx, "groups.create", values)
if err != nil {
return nil, err
}
@ -109,32 +106,13 @@ func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (*
"channel": {group},
}
response, err := groupRequest(ctx, api.httpclient, "groups.createChild", values, api.debug)
response, err := api.groupRequest(ctx, "groups.createChild", values)
if err != nil {
return nil, err
}
return &response.Group, nil
}
// CloseGroup closes a private group
func (api *Client) CloseGroup(group string) (bool, bool, error) {
return api.CloseGroupContext(context.Background(), group)
}
// CloseGroupContext closes a private group with a custom context
func (api *Client) CloseGroupContext(ctx context.Context, group string) (bool, bool, error) {
values := url.Values{
"token": {api.token},
"channel": {group},
}
response, err := imRequest(ctx, api.httpclient, "groups.close", values, api.debug)
if err != nil {
return false, false, err
}
return response.NoOp, response.AlreadyClosed, nil
}
// GetGroupHistory fetches all the history for a private group
func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) {
return api.GetGroupHistoryContext(context.Background(), group, params)
@ -170,7 +148,7 @@ func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, par
}
}
response, err := groupRequest(ctx, api.httpclient, "groups.history", values, api.debug)
response, err := api.groupRequest(ctx, "groups.history", values)
if err != nil {
return nil, err
}
@ -190,7 +168,7 @@ func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user str
"user": {user},
}
response, err := groupRequest(ctx, api.httpclient, "groups.invite", values, api.debug)
response, err := api.groupRequest(ctx, "groups.invite", values)
if err != nil {
return nil, false, err
}
@ -209,7 +187,7 @@ func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err err
"channel": {group},
}
_, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug)
_, err = api.groupRequest(ctx, "groups.leave", values)
return err
}
@ -226,7 +204,7 @@ func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user str
"user": {user},
}
_, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug)
_, err = api.groupRequest(ctx, "groups.kick", values)
return err
}
@ -244,7 +222,7 @@ func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) (
values.Add("exclude_archived", "1")
}
response, err := groupRequest(ctx, api.httpclient, "groups.list", values, api.debug)
response, err := api.groupRequest(ctx, "groups.list", values)
if err != nil {
return nil, err
}
@ -259,11 +237,12 @@ func (api *Client) GetGroupInfo(group string) (*Group, error) {
// GetGroupInfoContext retrieves the given group with a custom context
func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) {
values := url.Values{
"token": {api.token},
"channel": {group},
"token": {api.token},
"channel": {group},
"include_locale": {strconv.FormatBool(true)},
}
response, err := groupRequest(ctx, api.httpclient, "groups.info", values, api.debug)
response, err := api.groupRequest(ctx, "groups.info", values)
if err != nil {
return nil, err
}
@ -288,7 +267,7 @@ func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string
"ts": {ts},
}
_, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug)
_, err = api.groupRequest(ctx, "groups.mark", values)
return err
}
@ -304,7 +283,7 @@ func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bo
"channel": {group},
}
response, err := groupRequest(ctx, api.httpclient, "groups.open", values, api.debug)
response, err := api.groupRequest(ctx, "groups.open", values)
if err != nil {
return false, false, err
}
@ -328,7 +307,7 @@ func (api *Client) RenameGroupContext(ctx context.Context, group, name string) (
// XXX: the created entry in this call returns a string instead of a number
// so I may have to do some workaround to solve it.
response, err := groupRequest(ctx, api.httpclient, "groups.rename", values, api.debug)
response, err := api.groupRequest(ctx, "groups.rename", values)
if err != nil {
return nil, err
}
@ -348,7 +327,7 @@ func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose st
"purpose": {purpose},
}
response, err := groupRequest(ctx, api.httpclient, "groups.setPurpose", values, api.debug)
response, err := api.groupRequest(ctx, "groups.setPurpose", values)
if err != nil {
return "", err
}
@ -368,7 +347,7 @@ func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string
"topic": {topic},
}
response, err := groupRequest(ctx, api.httpclient, "groups.setTopic", values, api.debug)
response, err := api.groupRequest(ctx, "groups.setTopic", values)
if err != nil {
return "", err
}

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

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -23,22 +22,18 @@ type imResponseFull struct {
// IM contains information related to the Direct Message channel
type IM struct {
conversation
IsIM bool `json:"is_im"`
User string `json:"user"`
IsUserDeleted bool `json:"is_user_deleted"`
Conversation
IsUserDeleted bool `json:"is_user_deleted"`
}
func imRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*imResponseFull, error) {
func (api *Client) imRequest(ctx context.Context, path string, values url.Values) (*imResponseFull, error) {
response := &imResponseFull{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// CloseIMChannel closes the direct message channel
@ -53,7 +48,7 @@ func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (b
"channel": {channel},
}
response, err := imRequest(ctx, api.httpclient, "im.close", values, api.debug)
response, err := api.imRequest(ctx, "im.close", values)
if err != nil {
return false, false, err
}
@ -74,7 +69,7 @@ func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool,
"user": {user},
}
response, err := imRequest(ctx, api.httpclient, "im.open", values, api.debug)
response, err := api.imRequest(ctx, "im.open", values)
if err != nil {
return false, false, "", err
}
@ -94,7 +89,7 @@ func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string)
"ts": {ts},
}
_, err := imRequest(ctx, api.httpclient, "im.mark", values, api.debug)
_, err := api.imRequest(ctx, "im.mark", values)
return err
}
@ -133,7 +128,7 @@ func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, para
}
}
response, err := imRequest(ctx, api.httpclient, "im.history", values, api.debug)
response, err := api.imRequest(ctx, "im.history", values)
if err != nil {
return nil, err
}
@ -151,7 +146,7 @@ func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) {
"token": {api.token},
}
response, err := imRequest(ctx, api.httpclient, "im.list", values, api.debug)
response, err := api.imRequest(ctx, "im.list", values)
if err != nil {
return nil, err
}

98
vendor/github.com/nlopes/slack/interactions.go generated vendored Normal file
View File

@ -0,0 +1,98 @@
package slack
import (
"encoding/json"
)
// InteractionType type of interactions
type InteractionType string
// ActionType type represents the type of action (attachment, block, etc.)
type actionType string
// action is an interface that should be implemented by all callback action types
type action interface {
actionType() actionType
}
// Types of interactions that can be received.
const (
InteractionTypeDialogCancellation = InteractionType("dialog_cancellation")
InteractionTypeDialogSubmission = InteractionType("dialog_submission")
InteractionTypeDialogSuggestion = InteractionType("dialog_suggestion")
InteractionTypeInteractionMessage = InteractionType("interactive_message")
InteractionTypeMessageAction = InteractionType("message_action")
InteractionTypeBlockActions = InteractionType("block_actions")
)
// InteractionCallback is sent from slack when a user interactions with a button or dialog.
type InteractionCallback struct {
Type InteractionType `json:"type"`
Token string `json:"token"`
CallbackID string `json:"callback_id"`
ResponseURL string `json:"response_url"`
TriggerID string `json:"trigger_id"`
ActionTs string `json:"action_ts"`
Team Team `json:"team"`
Channel Channel `json:"channel"`
User User `json:"user"`
OriginalMessage Message `json:"original_message"`
Message Message `json:"message"`
Name string `json:"name"`
Value string `json:"value"`
MessageTs string `json:"message_ts"`
AttachmentID string `json:"attachment_id"`
ActionCallback ActionCallbacks `json:"actions"`
DialogSubmissionCallback
}
// ActionCallback is a convenience struct defined to allow dynamic unmarshalling of
// the "actions" value in Slack's JSON response, which varies depending on block type
type ActionCallbacks struct {
AttachmentActions []*AttachmentAction
BlockActions []*BlockAction
}
// UnmarshalJSON implements the Marshaller interface in order to delegate
// marshalling and allow for proper type assertion when decoding the response
func (a *ActionCallbacks) UnmarshalJSON(data []byte) error {
var raw []json.RawMessage
err := json.Unmarshal(data, &raw)
if err != nil {
return err
}
for _, r := range raw {
var obj map[string]interface{}
err := json.Unmarshal(r, &obj)
if err != nil {
return err
}
if _, ok := obj["block_id"].(string); ok {
action, err := unmarshalAction(r, &BlockAction{})
if err != nil {
return err
}
a.BlockActions = append(a.BlockActions, action.(*BlockAction))
return nil
}
action, err := unmarshalAction(r, &AttachmentAction{})
if err != nil {
return err
}
a.AttachmentActions = append(a.AttachmentActions, action.(*AttachmentAction))
}
return nil
}
func unmarshalAction(r json.RawMessage, callbackAction action) (action, error) {
err := json.Unmarshal(r, callbackAction)
if err != nil {
return nil, err
}
return callbackAction, nil
}

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

@ -2,52 +2,59 @@ package slack
import (
"fmt"
"sync"
)
// SetLogger let's library users supply a logger, so that api debugging
// can be logged along with the application's debugging info.
func SetLogger(l logProvider) {
loggerMutex.Lock()
logger = ilogger{logProvider: l}
loggerMutex.Unlock()
}
var (
loggerMutex = new(sync.Mutex)
logger logInternal // A logger that can be set by consumers
)
// logProvider is a logger interface compatible with both stdlib and some
// 3rd party loggers such as logrus.
type logProvider interface {
// logger is a logger interface compatible with both stdlib and some
// 3rd party loggers.
type logger interface {
Output(int, string) error
}
// logInternal represents the internal logging api we use.
type logInternal interface {
// ilogger represents the internal logging api we use.
type ilogger interface {
logger
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Output(int, string) error
}
// ilogger implements the additional methods used by our internal logging.
type ilogger struct {
logProvider
type debug interface {
Debug() bool
// Debugf print a formatted debug line.
Debugf(format string, v ...interface{})
// Debugln print a debug line.
Debugln(v ...interface{})
}
// internalLog implements the additional methods used by our internal logging.
type internalLog struct {
logger
}
// Println replicates the behaviour of the standard logger.
func (t ilogger) Println(v ...interface{}) {
func (t internalLog) Println(v ...interface{}) {
t.Output(2, fmt.Sprintln(v...))
}
// Printf replicates the behaviour of the standard logger.
func (t ilogger) Printf(format string, v ...interface{}) {
func (t internalLog) Printf(format string, v ...interface{}) {
t.Output(2, fmt.Sprintf(format, v...))
}
// Print replicates the behaviour of the standard logger.
func (t ilogger) Print(v ...interface{}) {
func (t internalLog) Print(v ...interface{}) {
t.Output(2, fmt.Sprint(v...))
}
type discard struct{}
func (t discard) Debug() bool {
return false
}
// Debugf print a formatted debug line.
func (t discard) Debugf(format string, v ...interface{}) {}
// Debugln print a debug line.
func (t discard) Debugln(v ...interface{}) {}

View File

@ -4,17 +4,19 @@ package slack
type OutgoingMessage struct {
ID int `json:"id"`
// channel ID
Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"`
Type string `json:"type,omitempty"`
ThreadTimestamp string `json:"thread_ts,omitempty"`
ThreadBroadcast bool `json:"reply_broadcast,omitempty"`
Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"`
Type string `json:"type,omitempty"`
ThreadTimestamp string `json:"thread_ts,omitempty"`
ThreadBroadcast bool `json:"reply_broadcast,omitempty"`
IDs []string `json:"ids,omitempty"`
}
// Message is an auxiliary type to allow us to have a message containing sub messages
type Message struct {
Msg
SubMessage *Msg `json:"message,omitempty"`
PreviousMessage *Msg `json:"previous_message,omitempty"`
}
// Msg contains information about a slack message
@ -91,6 +93,9 @@ 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"`
}
// Icon is used for bot messages
@ -147,6 +152,15 @@ func (rtm *RTM) NewOutgoingMessage(text string, channelID string, options ...RTM
return &msg
}
// NewSubscribeUserPresence prepares an OutgoingMessage that the user can
// use to subscribe presence events for the specified users.
func (rtm *RTM) NewSubscribeUserPresence(ids []string) *OutgoingMessage {
return &OutgoingMessage{
Type: "presence_sub",
IDs: ids,
}
}
// NewTypingMessage prepares an OutgoingMessage that the user can
// use to send as a typing indicator. Use this function to properly set the
// messageID.
@ -174,5 +188,4 @@ func RTMsgOptionBroadcast() RTMsgOption {
return func(msg *OutgoingMessage) {
msg.ThreadBroadcast = true
}
}

View File

@ -19,6 +19,7 @@ import (
"time"
)
// SlackResponse handles parsing out errors from the web api.
type SlackResponse struct {
Ok bool `json:"ok"`
Error string `json:"error"`
@ -47,64 +48,89 @@ type statusCodeError struct {
}
func (t statusCodeError) Error() string {
// TODO: this is a bad error string, should clean it up with a breaking changes
// merger.
return fmt.Sprintf("Slack server error: %s.", t.Status)
return fmt.Sprintf("slack server error: %s", t.Status)
}
func (t statusCodeError) HTTPStatusCode() int {
return t.Code
}
func (t statusCodeError) Retryable() bool {
if t.Code >= 500 || t.Code == http.StatusTooManyRequests {
return true
}
return false
}
// RateLimitedError represents the rate limit respond from slack
type RateLimitedError struct {
RetryAfter time.Duration
}
func (e *RateLimitedError) Error() string {
return fmt.Sprintf("Slack rate limit exceeded, retry after %s", e.RetryAfter)
return fmt.Sprintf("slack rate limit exceeded, retry after %s", e.RetryAfter)
}
func fileUploadReq(ctx context.Context, path, fieldname, filename string, values url.Values, r io.Reader) (*http.Request, error) {
body := &bytes.Buffer{}
wr := multipart.NewWriter(body)
func (e *RateLimitedError) Retryable() bool {
return true
}
ioWriter, err := wr.CreateFormFile(fieldname, filename)
func fileUploadReq(ctx context.Context, path string, values url.Values, r io.Reader) (*http.Request, error) {
req, err := http.NewRequest("POST", path, r)
if err != nil {
wr.Close()
return nil, err
}
_, err = io.Copy(ioWriter, r)
if err != nil {
wr.Close()
return nil, err
}
// Close the multipart writer or the footer won't be written
wr.Close()
req, err := http.NewRequest("POST", path, body)
req = req.WithContext(ctx)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", wr.FormDataContentType())
req.URL.RawQuery = (values).Encode()
return req, nil
}
func parseResponseBody(body io.ReadCloser, intf interface{}, debug bool) error {
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 parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error {
response, err := ioutil.ReadAll(body)
if err != nil {
return err
}
// FIXME: will be api.Debugf
if debug {
logger.Printf("parseResponseBody: %s\n", string(response))
if d.Debug() {
d.Debugln("parseResponseBody", string(response))
}
return json.Unmarshal(response, intf)
}
func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error {
func postLocalWithMultipartResponse(ctx context.Context, client httpClient, path, fpath, fieldname string, values url.Values, intf interface{}, d debug) error {
fullpath, err := filepath.Abs(fpath)
if err != nil {
return err
@ -114,14 +140,57 @@ func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, p
return err
}
defer file.Close()
return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, debug)
return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, d)
}
func postWithMultipartResponse(ctx context.Context, client HTTPRequester, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, debug bool) error {
req, err := fileUploadReq(ctx, SLACK_API+path, fieldname, name, values, r)
func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, d debug) error {
pipeReader, pipeWriter := io.Pipe()
wr := multipart.NewWriter(pipeWriter)
errc := make(chan error)
go func() {
defer pipeWriter.Close()
ioWriter, err := wr.CreateFormFile(fieldname, name)
if err != nil {
errc <- err
return
}
_, err = io.Copy(ioWriter, r)
if err != nil {
errc <- err
return
}
if err = wr.Close(); err != nil {
errc <- err
return
}
}()
req, err := fileUploadReq(ctx, path, values, pipeReader)
if err != nil {
return err
}
req.Header.Add("Content-Type", wr.FormDataContentType())
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
err = checkStatusCode(resp, d)
if err != nil {
return err
}
select {
case err = <-errc:
return err
default:
return parseResponseBody(resp.Body, intf, d)
}
}
func doPost(ctx context.Context, client httpClient, req *http.Request, intf interface{}, d debug) error {
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
@ -129,50 +198,16 @@ func postWithMultipartResponse(ctx context.Context, client HTTPRequester, path,
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != http.StatusOK {
logResponse(resp, debug)
return statusCodeError{Code: resp.StatusCode, Status: resp.Status}
}
return parseResponseBody(resp.Body, intf, debug)
}
func doPost(ctx context.Context, client HTTPRequester, req *http.Request, intf interface{}, debug bool) error {
req = req.WithContext(ctx)
resp, err := client.Do(req)
err = checkStatusCode(resp, d)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != http.StatusOK {
logResponse(resp, debug)
return statusCodeError{Code: resp.StatusCode, Status: resp.Status}
}
return parseResponseBody(resp.Body, intf, debug)
return parseResponseBody(resp.Body, intf, d)
}
// post JSON.
func postJSON(ctx context.Context, client HTTPRequester, endpoint, token string, json []byte, intf interface{}, debug bool) error {
func postJSON(ctx context.Context, client httpClient, endpoint, token string, json []byte, intf interface{}, d debug) error {
reqBody := bytes.NewBuffer(json)
req, err := http.NewRequest("POST", endpoint, reqBody)
if err != nil {
@ -180,38 +215,43 @@ func postJSON(ctx context.Context, client HTTPRequester, endpoint, token string,
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return doPost(ctx, client, req, intf, debug)
return doPost(ctx, client, req, intf, d)
}
// post a url encoded form.
func postForm(ctx context.Context, client HTTPRequester, endpoint string, values url.Values, intf interface{}, debug bool) error {
func postForm(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error {
reqBody := strings.NewReader(values.Encode())
req, err := http.NewRequest("POST", endpoint, reqBody)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return doPost(ctx, client, req, intf, debug)
return doPost(ctx, client, req, intf, d)
}
// post to a slack web method.
func postSlackMethod(ctx context.Context, client HTTPRequester, path string, values url.Values, intf interface{}, debug bool) error {
return postForm(ctx, client, SLACK_API+path, values, intf, debug)
func getResource(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error {
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.URL.RawQuery = values.Encode()
return doPost(ctx, client, req, intf, d)
}
func parseAdminResponse(ctx context.Context, client HTTPRequester, method string, teamName string, values url.Values, intf interface{}, debug bool) error {
endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix())
return postForm(ctx, client, endpoint, values, intf, debug)
func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf interface{}, d debug) error {
endpoint := fmt.Sprintf(WEBAPIURLFormat, teamName, method, time.Now().Unix())
return postForm(ctx, client, endpoint, values, intf, d)
}
func logResponse(resp *http.Response, debug bool) error {
if debug {
func logResponse(resp *http.Response, d debug) error {
if d.Debug() {
text, err := httputil.DumpResponse(resp, true)
if err != nil {
return err
}
logger.Print(string(text))
d.Debugln(string(text))
}
return nil
@ -225,12 +265,6 @@ func okJSONHandler(rw http.ResponseWriter, r *http.Request) {
rw.Write(response)
}
type errorString string
func (t errorString) Error() string {
return string(t)
}
// timerReset safely reset a timer, see time.Timer.Reset for details.
func timerReset(t *time.Timer, d time.Duration) {
if !t.Stop() {
@ -238,3 +272,21 @@ 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
}

View File

@ -2,10 +2,10 @@ package slack
import (
"context"
"errors"
"net/url"
)
// OAuthResponseIncomingWebhook ...
type OAuthResponseIncomingWebhook struct {
URL string `json:"url"`
Channel string `json:"channel"`
@ -13,11 +13,13 @@ type OAuthResponseIncomingWebhook struct {
ConfigurationURL string `json:"configuration_url"`
}
// OAuthResponseBot ...
type OAuthResponseBot struct {
BotUserID string `json:"bot_user_id"`
BotAccessToken string `json:"bot_access_token"`
}
// OAuthResponse ...
type OAuthResponse struct {
AccessToken string `json:"access_token"`
Scope string `json:"scope"`
@ -30,24 +32,24 @@ type OAuthResponse struct {
}
// GetOAuthToken retrieves an AccessToken
func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) {
return GetOAuthTokenContext(context.Background(), clientID, clientSecret, code, redirectURI, debug)
func GetOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) {
return GetOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
}
// GetOAuthTokenContext retrieves an AccessToken with a custom context
func GetOAuthTokenContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) {
response, err := GetOAuthResponseContext(ctx, clientID, clientSecret, code, redirectURI, debug)
func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) {
response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI)
if err != nil {
return "", "", err
}
return response.AccessToken, response.Scope, nil
}
func GetOAuthResponse(clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) {
return GetOAuthResponseContext(context.Background(), clientID, clientSecret, code, redirectURI, debug)
func GetOAuthResponse(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) {
return GetOAuthResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
}
func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) {
func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) {
values := url.Values{
"client_id": {clientID},
"client_secret": {clientSecret},
@ -55,12 +57,8 @@ func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code,
"redirect_uri": {redirectURI},
}
response := &OAuthResponse{}
err = postSlackMethod(ctx, customHTTPClient, "oauth.access", values, response, debug)
if err != nil {
if err = postForm(ctx, client, APIURL+"oauth.access", values, response, discard{}); err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}

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.debug); err != nil {
if err := api.postMethod(ctx, "pins.add", values, response); err != nil {
return err
}
@ -63,7 +63,7 @@ func (api *Client) RemovePinContext(ctx context.Context, channel string, item It
}
response := &SlackResponse{}
if err := postSlackMethod(ctx, api.httpclient, "pins.remove", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "pins.remove", values, response); err != nil {
return err
}
@ -83,7 +83,7 @@ func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item,
}
response := &listPinsResponseFull{}
err := postSlackMethod(ctx, api.httpclient, "pins.list", values, response, api.debug)
err := api.postMethod(ctx, "pins.list", values, response)
if err != nil {
return nil, nil, err
}

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.debug); err != nil {
if err := api.postMethod(ctx, "reactions.add", values, response); err != nil {
return err
}
@ -189,7 +188,7 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item
}
response := &SlackResponse{}
if err := postSlackMethod(ctx, api.httpclient, "reactions.remove", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "reactions.remove", values, response); err != nil {
return err
}
@ -223,12 +222,14 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params
}
response := &getReactionsResponseFull{}
if err := postSlackMethod(ctx, api.httpclient, "reactions.get", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "reactions.get", values, response); err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, err
}
return response.extractReactions(), nil
}
@ -256,12 +257,14 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction
}
response := &listReactionsResponseFull{}
err := postSlackMethod(ctx, api.httpclient, "reactions.list", values, response, api.debug)
err := api.postMethod(ctx, "reactions.list", values, response)
if err != nil {
return nil, nil, err
}
if !response.Ok {
return nil, nil, errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, nil, err
}
return response.extractReactedItems(), &response.Paging, nil
}

75
vendor/github.com/nlopes/slack/reminders.go generated vendored Normal file
View File

@ -0,0 +1,75 @@
package slack
import (
"context"
"net/url"
"time"
)
type Reminder struct {
ID string `json:"id"`
Creator string `json:"creator"`
User string `json:"user"`
Text string `json:"text"`
Recurring bool `json:"recurring"`
Time time.Time `json:"time"`
CompleteTS int `json:"complete_ts"`
}
type reminderResp struct {
SlackResponse
Reminder Reminder `json:"reminder"`
}
func (api *Client) doReminder(ctx context.Context, path string, values url.Values) (*Reminder, error) {
response := &reminderResp{}
if err := api.postMethod(ctx, path, values, response); err != nil {
return nil, err
}
return &response.Reminder, response.Err()
}
// AddChannelReminder adds a reminder for a channel.
//
// See https://api.slack.com/methods/reminders.add (NOTE: the ability to set
// reminders on a channel is currently undocumented but has been tested to
// work)
func (api *Client) AddChannelReminder(channelID, text, time string) (*Reminder, error) {
values := url.Values{
"token": {api.token},
"text": {text},
"time": {time},
"channel": {channelID},
}
return api.doReminder(context.Background(), "reminders.add", values)
}
// AddUserReminder adds a reminder for a user.
//
// See https://api.slack.com/methods/reminders.add (NOTE: the ability to set
// reminders on a channel is currently undocumented but has been tested to
// work)
func (api *Client) AddUserReminder(userID, text, time string) (*Reminder, error) {
values := url.Values{
"token": {api.token},
"text": {text},
"time": {time},
"user": {userID},
}
return api.doReminder(context.Background(), "reminders.add", values)
}
// DeleteReminder deletes an existing reminder.
//
// See https://api.slack.com/methods/reminders.delete
func (api *Client) DeleteReminder(id string) error {
values := url.Values{
"token": {api.token},
"reminder": {id},
}
response := &SlackResponse{}
if err := api.postMethod(context.Background(), "reminders.delete", values, response); err != nil {
return err
}
return response.Err()
}

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.debug)
err = api.postMethod(ctx, "rtm.start", url.Values{"token": {api.token}}, response)
if err != nil {
return nil, "", err
}
@ -63,7 +63,7 @@ func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) {
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
response := &infoResponseFull{}
err = postSlackMethod(ctx, api.httpclient, "rtm.connect", url.Values{"token": {api.token}}, response, api.debug)
err = api.postMethod(ctx, "rtm.connect", url.Values{"token": {api.token}}, response)
if err != nil {
api.Debugf("Failed to connect to RTM: %s", err)
return nil, "", err
@ -100,6 +100,13 @@ func RTMOptionPingInterval(d time.Duration) RTMOption {
}
}
// RTMOptionConnParams installs parameters to embed into the connection URL.
func RTMOptionConnParams(connParams url.Values) RTMOption {
return func(rtm *RTM) {
rtm.connParams = connParams
}
}
// NewRTM returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
func (api *Client) NewRTM(options ...RTMOption) *RTM {
@ -109,10 +116,9 @@ func (api *Client) NewRTM(options ...RTMOption) *RTM {
outgoingMessages: make(chan OutgoingMessage, 20),
pingInterval: defaultPingInterval,
pingDeadman: time.NewTimer(deadmanDuration(defaultPingInterval)),
isConnected: false,
wasIntentional: true,
killChannel: make(chan bool),
disconnected: make(chan struct{}, 1),
disconnected: make(chan struct{}),
disconnectedm: &sync.Once{},
forcePing: make(chan bool),
rawEvents: make(chan json.RawMessage),
idGen: NewSafeID(1),
@ -125,14 +131,3 @@ func (api *Client) NewRTM(options ...RTMOption) *RTM {
return result
}
// NewRTMWithOptions Deprecated just use NewRTM(RTMOptionsUseStart(true))
// returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
// This also allows to configure various options available for RTM API.
func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM {
if options != nil {
return api.NewRTM(RTMOptionUseStart(options.UseRTMStart))
}
return api.NewRTM()
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -104,14 +103,12 @@ func (api *Client) _search(ctx context.Context, path, query string, params Searc
}
response = &searchResponseFull{}
err := postSlackMethod(ctx, api.httpclient, path, values, response, api.debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}

View File

@ -1,47 +1,100 @@
package slack
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"hash"
"net/http"
)
// SecretsVerifier contains the information needed to verify that the request comes from Slack
type SecretsVerifier struct {
slackSig string
timeStamp string
hmac hash.Hash
}
// NewSecretsVerifier returns a SecretsVerifier object in exchange for an http.Header object and signing secret
func NewSecretsVerifier(header http.Header, signingSecret string) (SecretsVerifier, error) {
if header["X-Slack-Signature"][0] == "" || header["X-Slack-Request-Timestamp"][0] == "" {
return SecretsVerifier{}, errors.New("Headers are empty, cannot create SecretsVerifier")
}
hash := hmac.New(sha256.New, []byte(signingSecret))
hash.Write([]byte(fmt.Sprintf("v0:%s:", header["X-Slack-Request-Timestamp"][0])))
return SecretsVerifier{
slackSig: header["X-Slack-Signature"][0],
timeStamp: header["X-Slack-Request-Timestamp"][0],
hmac: hash,
}, nil
}
func (v *SecretsVerifier) Write(body []byte) (n int, err error) {
return v.hmac.Write(body)
}
// Ensure compares the signature sent from Slack with the actual computed hash to judge validity
func (v SecretsVerifier) Ensure() error {
computed := "v0=" + string(hex.EncodeToString(v.hmac.Sum(nil)))
if computed == v.slackSig {
return nil
}
return fmt.Errorf("Expected signing signature: %s, but computed: %s", v.slackSig, computed)
}
package slack
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"hash"
"net/http"
"strconv"
"strings"
"time"
)
// Signature headers
const (
hSignature = "X-Slack-Signature"
hTimestamp = "X-Slack-Request-Timestamp"
)
// SecretsVerifier contains the information needed to verify that the request comes from Slack
type SecretsVerifier struct {
signature []byte
hmac hash.Hash
}
func unsafeSignatureVerifier(header http.Header, secret string) (_ SecretsVerifier, err error) {
var (
bsignature []byte
)
signature := header.Get(hSignature)
stimestamp := header.Get(hTimestamp)
if signature == "" || stimestamp == "" {
return SecretsVerifier{}, ErrMissingHeaders
}
if bsignature, err = hex.DecodeString(strings.TrimPrefix(signature, "v0=")); err != nil {
return SecretsVerifier{}, err
}
hash := hmac.New(sha256.New, []byte(secret))
if _, err = hash.Write([]byte(fmt.Sprintf("v0:%s:", stimestamp))); err != nil {
return SecretsVerifier{}, err
}
return SecretsVerifier{
signature: bsignature,
hmac: hash,
}, nil
}
// NewSecretsVerifier returns a SecretsVerifier object in exchange for an http.Header object and signing secret
func NewSecretsVerifier(header http.Header, secret string) (sv SecretsVerifier, err error) {
var (
timestamp int64
)
stimestamp := header.Get(hTimestamp)
if sv, err = unsafeSignatureVerifier(header, secret); err != nil {
return SecretsVerifier{}, err
}
if timestamp, err = strconv.ParseInt(stimestamp, 10, 64); err != nil {
return SecretsVerifier{}, err
}
diff := absDuration(time.Since(time.Unix(timestamp, 0)))
if diff > 5*time.Minute {
return SecretsVerifier{}, ErrExpiredTimestamp
}
return sv, err
}
func (v *SecretsVerifier) Write(body []byte) (n int, err error) {
return v.hmac.Write(body)
}
// Ensure compares the signature sent from Slack with the actual computed hash to judge validity
func (v SecretsVerifier) Ensure() error {
computed := v.hmac.Sum(nil)
// use hmac.Equal prevent leaking timing information.
if hmac.Equal(computed, v.signature) {
return nil
}
return fmt.Errorf("Expected signing signature: %s, but computed: %s", hex.EncodeToString(v.signature), hex.EncodeToString(computed))
}
func abs64(n int64) int64 {
y := n >> 63
return (n ^ y) - y
}
func absDuration(n time.Duration) time.Duration {
return time.Duration(abs64(int64(n)))
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"fmt"
"log"
"net/http"
@ -10,31 +9,18 @@ import (
"os"
)
// Added as a var so that we can change this for testing purposes
var SLACK_API string = "https://slack.com/api/"
var SLACK_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s"
const (
// APIURL of the slack api.
APIURL = "https://slack.com/api/"
// WEBAPIURLFormat ...
WEBAPIURLFormat = "https://%s.slack.com/api/users.admin.%s?t=%d"
)
// HTTPClient sets a custom http.Client
// deprecated: in favor of SetHTTPClient()
var HTTPClient = &http.Client{}
var customHTTPClient HTTPRequester = HTTPClient
// HTTPRequester defines the minimal interface needed for an http.Client to be implemented.
//
// Use it in conjunction with the SetHTTPClient function to allow for other capabilities
// like a tracing http.Client
type HTTPRequester interface {
// httpClient defines the minimal interface needed for an http.Client to be implemented.
type httpClient interface {
Do(*http.Request) (*http.Response, error)
}
// SetHTTPClient allows you to specify a custom http.Client
// Use this instead of the package level HTTPClient variable if you want to use a custom client like the
// Stackdriver Trace HTTPClient https://godoc.org/cloud.google.com/go/trace#HTTPClient
func SetHTTPClient(client HTTPRequester) {
customHTTPClient = client
}
// ResponseMetadata holds pagination metadata
type ResponseMetadata struct {
Cursor string `json:"next_cursor"`
@ -48,12 +34,15 @@ func (t *ResponseMetadata) initialize() *ResponseMetadata {
return &ResponseMetadata{}
}
// AuthTestResponse ...
type AuthTestResponse struct {
URL string `json:"url"`
Team string `json:"team"`
User string `json:"user"`
TeamID string `json:"team_id"`
UserID string `json:"user_id"`
// EnterpriseID is only returned when an enterprise id present
EnterpriseID string `json:"enterprise_id,omitempty"`
}
type authTestResponseFull struct {
@ -61,28 +50,51 @@ type authTestResponseFull struct {
AuthTestResponse
}
// Client for the slack api.
type Client struct {
token string
info Info
endpoint string
debug bool
httpclient HTTPRequester
log ilogger
httpclient httpClient
}
// Option defines an option for a Client
type Option func(*Client)
// OptionHTTPClient - provide a custom http client to the slack client.
func OptionHTTPClient(c HTTPRequester) func(*Client) {
return func(s *Client) {
s.httpclient = c
func OptionHTTPClient(client httpClient) func(*Client) {
return func(c *Client) {
c.httpclient = client
}
}
// OptionDebug enable debugging for the client
func OptionDebug(b bool) func(*Client) {
return func(c *Client) {
c.debug = b
}
}
// OptionLog set logging for client.
func OptionLog(l logger) func(*Client) {
return func(c *Client) {
c.log = internalLog{logger: l}
}
}
// OptionAPIURL set the url for the client. only useful for testing.
func OptionAPIURL(u string) func(*Client) {
return func(c *Client) { c.endpoint = u }
}
// New builds a slack client from the provided token and options.
func New(token string, options ...Option) *Client {
s := &Client{
token: token,
httpclient: customHTTPClient,
endpoint: APIURL,
httpclient: &http.Client{},
log: log.New(os.Stderr, "nlopes/slack", log.LstdFlags|log.Lshortfile),
}
for _, opt := range options {
@ -98,43 +110,42 @@ func (api *Client) AuthTest() (response *AuthTestResponse, error error) {
}
// AuthTestContext tests if the user is able to do authenticated requests or not with a custom context
func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, error error) {
func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, err error) {
api.Debugf("Challenging auth...")
responseFull := &authTestResponseFull{}
err := postSlackMethod(ctx, api.httpclient, "auth.test", url.Values{"token": {api.token}}, responseFull, api.debug)
err = api.postMethod(ctx, "auth.test", url.Values{"token": {api.token}}, responseFull)
if err != nil {
api.Debugf("failed to test for auth: %s", err)
return nil, err
}
if !responseFull.Ok {
api.Debugf("auth response was not Ok: %s", responseFull.Error)
return nil, errors.New(responseFull.Error)
}
api.Debugf("Auth challenge was successful with response %+v", responseFull.AuthTestResponse)
return &responseFull.AuthTestResponse, nil
}
// SetDebug switches the api into debug mode
// When in debug mode, it logs various info about what its doing
// If you ever use this in production, don't call SetDebug(true)
func (api *Client) SetDebug(debug bool) {
api.debug = debug
if debug && logger == nil {
SetLogger(log.New(os.Stdout, "nlopes/slack", log.LstdFlags|log.Lshortfile))
}
return &responseFull.AuthTestResponse, responseFull.Err()
}
// Debugf print a formatted debug line.
func (api *Client) Debugf(format string, v ...interface{}) {
if api.debug {
logger.Output(2, fmt.Sprintf(format, v...))
api.log.Output(2, fmt.Sprintf(format, v...))
}
}
// Debugln print a debug line.
func (api *Client) Debugln(v ...interface{}) {
if api.debug {
logger.Output(2, fmt.Sprintln(v...))
api.log.Output(2, fmt.Sprintln(v...))
}
}
// Debug returns if debug is enabled.
func (api *Client) Debug() bool {
return api.debug
}
// post to a slack web method.
func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf interface{}) error {
return postForm(ctx, api.httpclient, api.endpoint+path, values, intf, api)
}
// get a slack web method.
func (api *Client) getMethod(ctx context.Context, path string, values url.Values, intf interface{}) error {
return getResource(ctx, api.httpclient, api.endpoint+path, values, intf, api)
}

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.debug); err != nil {
if err := api.postMethod(ctx, "stars.add", values, response); err != nil {
return err
}
@ -87,7 +86,7 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I
}
response := &SlackResponse{}
if err := postSlackMethod(ctx, api.httpclient, "stars.remove", values, response, api.debug); err != nil {
if err := api.postMethod(ctx, "stars.remove", values, response); err != nil {
return err
}
@ -115,13 +114,15 @@ func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters)
}
response := &listResponseFull{}
err := postSlackMethod(ctx, api.httpclient, "stars.list", values, response, api.debug)
err := api.postMethod(ctx, "stars.list", values, response)
if err != nil {
return nil, nil, err
}
if !response.Ok {
return nil, nil, errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, nil, err
}
return response.Items, &response.Paging, nil
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strconv"
)
@ -67,44 +66,33 @@ func NewAccessLogParameters() AccessLogParameters {
}
}
func teamRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*TeamResponse, error) {
func (api *Client) teamRequest(ctx context.Context, path string, values url.Values) (*TeamResponse, error) {
response := &TeamResponse{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
func billableInfoRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (map[string]BillingActive, error) {
func (api *Client) billableInfoRequest(ctx context.Context, path string, values url.Values) (map[string]BillingActive, error) {
response := &BillableInfoResponse{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.BillableInfo, nil
return response.BillableInfo, response.Err()
}
func accessLogsRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*LoginResponse, error) {
func (api *Client) accessLogsRequest(ctx context.Context, path string, values url.Values) (*LoginResponse, error) {
response := &LoginResponse{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// GetTeamInfo gets the Team Information of the user
@ -118,7 +106,7 @@ func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) {
"token": {api.token},
}
response, err := teamRequest(ctx, api.httpclient, "team.info", values, api.debug)
response, err := api.teamRequest(ctx, "team.info", values)
if err != nil {
return nil, err
}
@ -142,24 +130,26 @@ func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogPar
values.Add("page", strconv.Itoa(params.Page))
}
response, err := accessLogsRequest(ctx, api.httpclient, "team.accessLogs", values, api.debug)
response, err := api.accessLogsRequest(ctx, "team.accessLogs", values)
if err != nil {
return nil, nil, err
}
return response.Logins, &response.Paging, nil
}
// GetBillableInfo ...
func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error) {
return api.GetBillableInfoContext(context.Background(), user)
}
// GetBillableInfoContext ...
func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map[string]BillingActive, error) {
values := url.Values{
"token": {api.token},
"user": {user},
}
return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug)
return api.billableInfoRequest(ctx, "team.billableInfo", values)
}
// GetBillableInfoForTeam returns the billing_active status of all users on the team.
@ -173,5 +163,5 @@ func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[strin
"token": {api.token},
}
return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug)
return api.billableInfoRequest(ctx, "team.billableInfo", values)
}

View File

@ -2,7 +2,6 @@ package slack
import (
"context"
"errors"
"net/url"
"strings"
)
@ -41,16 +40,14 @@ type userGroupResponseFull struct {
SlackResponse
}
func userGroupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userGroupResponseFull, error) {
func (api *Client) userGroupRequest(ctx context.Context, path string, values url.Values) (*userGroupResponseFull, error) {
response := &userGroupResponseFull{}
err := postSlackMethod(ctx, client, path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// CreateUserGroup creates a new user group
@ -77,7 +74,7 @@ func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGro
values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")}
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.create", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.create", values)
if err != nil {
return UserGroup{}, err
}
@ -96,7 +93,7 @@ func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string
"usergroup": {userGroup},
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.disable", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.disable", values)
if err != nil {
return UserGroup{}, err
}
@ -115,7 +112,7 @@ func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string)
"usergroup": {userGroup},
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.enable", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.enable", values)
if err != nil {
return UserGroup{}, err
}
@ -179,7 +176,7 @@ func (api *Client) GetUserGroupsContext(ctx context.Context, options ...GetUserG
values.Add("include_users", "true")
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.list", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.list", values)
if err != nil {
return nil, err
}
@ -209,8 +206,12 @@ func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGro
if userGroup.Description != "" {
values["description"] = []string{userGroup.Description}
}
if len(userGroup.Prefs.Channels) > 0 {
values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")}
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.update", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.update", values)
if err != nil {
return UserGroup{}, err
}
@ -229,7 +230,7 @@ func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup str
"usergroup": {userGroup},
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.list", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.users.list", values)
if err != nil {
return []string{}, err
}
@ -249,7 +250,7 @@ func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup
"users": {members},
}
response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.update", values, api.debug)
response, err := api.userGroupRequest(ctx, "usergroups.users.update", values)
if err != nil {
return UserGroup{}, err
}

View File

@ -3,7 +3,6 @@ package slack
import (
"context"
"encoding/json"
"errors"
"net/url"
"strconv"
)
@ -12,7 +11,6 @@ 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 +35,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 +99,30 @@ 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"`
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 +153,17 @@ type UserIdentity struct {
Image512 string `json:"image_512"`
}
// EnterpriseUser is present when a user is part of Slack Enterprise Grid
// https://api.slack.com/types/user#enterprise_grid_user_objects
type EnterpriseUser struct {
ID string `json:"id"`
EnterpriseID string `json:"enterprise_id"`
EnterpriseName string `json:"enterprise_name"`
IsAdmin bool `json:"is_admin"`
IsOwner bool `json:"is_owner"`
Teams []string `json:"teams"`
}
type TeamIdentity struct {
ID string `json:"id"`
Name string `json:"name"`
@ -189,16 +201,14 @@ func NewUserSetPhotoParams() UserSetPhotoParams {
}
}
func userRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userResponseFull, error) {
func (api *Client) userRequest(ctx context.Context, path string, values url.Values) (*userResponseFull, error) {
response := &userResponseFull{}
err := postForm(ctx, client, SLACK_API+path, values, response, debug)
err := api.postMethod(ctx, path, values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
return response, response.Err()
}
// GetUserPresence will retrieve the current presence status of given user.
@ -213,7 +223,7 @@ func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*Us
"user": {user},
}
response, err := userRequest(ctx, api.httpclient, "users.getPresence", values, api.debug)
response, err := api.userRequest(ctx, "users.getPresence", values)
if err != nil {
return nil, err
}
@ -228,11 +238,12 @@ func (api *Client) GetUserInfo(user string) (*User, error) {
// GetUserInfoContext will retrieve the complete user information with a custom context
func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) {
values := url.Values{
"token": {api.token},
"user": {user},
"token": {api.token},
"user": {user},
"include_locale": {strconv.FormatBool(true)},
}
response, err := userRequest(ctx, api.httpclient, "users.info", values, api.debug)
response, err := api.userRequest(ctx, "users.info", values)
if err != nil {
return nil, err
}
@ -304,13 +315,14 @@ func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error)
t.previousResp = t.previousResp.initialize()
values := url.Values{
"limit": {strconv.Itoa(t.limit)},
"presence": {strconv.FormatBool(t.presence)},
"token": {t.c.token},
"cursor": {t.previousResp.Cursor},
"limit": {strconv.Itoa(t.limit)},
"presence": {strconv.FormatBool(t.presence)},
"token": {t.c.token},
"cursor": {t.previousResp.Cursor},
"include_locale": {strconv.FormatBool(true)},
}
if resp, err = userRequest(ctx, t.c.httpclient, "users.list", values, t.c.debug); err != nil {
if resp, err = t.c.userRequest(ctx, "users.list", values); err != nil {
return t, err
}
@ -355,7 +367,7 @@ func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*Us
"token": {api.token},
"email": {email},
}
response, err := userRequest(ctx, api.httpclient, "users.lookupByEmail", values, api.debug)
response, err := api.userRequest(ctx, "users.lookupByEmail", values)
if err != nil {
return nil, err
}
@ -373,7 +385,7 @@ func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) {
"token": {api.token},
}
_, err = userRequest(ctx, api.httpclient, "users.setActive", values, api.debug)
_, err = api.userRequest(ctx, "users.setActive", values)
return err
}
@ -389,7 +401,7 @@ func (api *Client) SetUserPresenceContext(ctx context.Context, presence string)
"presence": {presence},
}
_, err := userRequest(ctx, api.httpclient, "users.setPresence", values, api.debug)
_, err := api.userRequest(ctx, "users.setPresence", values)
return err
}
@ -405,13 +417,15 @@ func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityRes
}
response := &UserIdentityResponse{}
err := postForm(ctx, api.httpclient, SLACK_API+"users.identity", values, response, api.debug)
err := api.postMethod(ctx, "users.identity", values, response)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
if err := response.Err(); err != nil {
return nil, err
}
return response, nil
}
@ -436,7 +450,7 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params
values.Add("crop_w", strconv.Itoa(params.CropW))
}
err := postLocalWithMultipartResponse(ctx, api.httpclient, "users.setPhoto", image, "image", values, response, api.debug)
err := postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", values, response, api)
if err != nil {
return err
}
@ -456,7 +470,7 @@ func (api *Client) DeleteUserPhotoContext(ctx context.Context) error {
"token": {api.token},
}
err := postForm(ctx, api.httpclient, SLACK_API+"users.deletePhoto", values, response, api.debug)
err := api.postMethod(ctx, "users.deletePhoto", values, response)
if err != nil {
return err
}
@ -467,15 +481,16 @@ 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.SetUserCustomStatusContext(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 {
// 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 +503,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,
},
)
@ -506,15 +523,11 @@ func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, s
}
response := &userResponseFull{}
if err = postForm(ctx, api.httpclient, SLACK_API+"users.profile.set", values, response, api.debug); err != nil {
if err = api.postMethod(ctx, "users.profile.set", values, response); err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
return response.Err()
}
// UnsetUserCustomStatus removes the custom status message for the currently
@ -526,7 +539,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 +560,14 @@ func (api *Client) GetUserProfileContext(ctx context.Context, userID string, inc
}
resp := &getUserProfileResponse{}
err := postSlackMethod(ctx, api.httpclient, "users.profile.get", values, &resp, api.debug)
err := api.postMethod(ctx, "users.profile.get", values, &resp)
if err != nil {
return nil, err
}
if !resp.Ok {
return nil, errors.New(resp.Error)
if err := resp.Err(); err != nil {
return nil, err
}
return resp.Profile, nil
}

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,7 @@ package slack
import (
"encoding/json"
"errors"
"net/url"
"sync"
"time"
@ -20,6 +20,9 @@ const (
//
// Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions)
type RTM struct {
// Client is the main API, embedded
Client
idGen IDGenerator
pingInterval time.Duration
pingDeadman *time.Timer
@ -29,15 +32,10 @@ type RTM struct {
IncomingEvents chan RTMEvent
outgoingMessages chan OutgoingMessage
killChannel chan bool
disconnected chan struct{} // disconnected is closed when Disconnect is invoked, regardless of connection state. Allows for ManagedConnection to not leak.
disconnected chan struct{}
disconnectedm *sync.Once
forcePing chan bool
rawEvents chan json.RawMessage
wasIntentional bool
isConnected bool
// Client is the main API, embedded
Client
websocketURL string
// UserDetails upon connection
info *Info
@ -53,40 +51,30 @@ type RTM struct {
// mu is mutex used to prevent RTM connection race conditions
mu *sync.Mutex
// connParams is a map of flags for connection parameters.
connParams url.Values
}
// RTMOptions allows configuration of various options available for RTM messaging
//
// This structure will evolve in time so please make sure you are always using the
// named keys for every entry available as per Go 1 compatibility promise adding fields
// to this structure should not be considered a breaking change.
type RTMOptions struct {
// UseRTMStart set to true in order to use rtm.start or false to use rtm.connect
// As of 11th July 2017 you should prefer setting this to false, see:
// https://api.slack.com/changelog/2017-04-start-using-rtm-connect-and-stop-using-rtm-start
UseRTMStart bool
// signal that we are disconnected by closing the channel.
// protect it with a mutex to ensure it only happens once.
func (rtm *RTM) disconnect() {
rtm.disconnectedm.Do(func() {
close(rtm.disconnected)
})
}
// Disconnect and wait, blocking until a successful disconnection.
func (rtm *RTM) Disconnect() error {
// avoid RTM disconnect race conditions
rtm.mu.Lock()
defer rtm.mu.Unlock()
// always push into the disconnected channel when invoked,
// always push into the kill channel when invoked,
// this lets the ManagedConnection() function properly clean up.
// if the buffer is full then just continue on.
select {
case rtm.disconnected <- struct{}{}:
default:
case rtm.killChannel <- true:
return nil
case <-rtm.disconnected:
return ErrAlreadyDisconnected
}
if !rtm.isConnected {
return errors.New("Invalid call to Disconnect - Slack API is already disconnected")
}
rtm.killChannel <- true
return nil
}
// GetInfo returns the info structure received when calling
@ -110,7 +98,7 @@ func (rtm *RTM) SendMessage(msg *OutgoingMessage) {
}
func (rtm *RTM) resetDeadman() {
timerReset(rtm.pingDeadman, deadmanDuration(rtm.pingInterval))
rtm.pingDeadman.Reset(deadmanDuration(rtm.pingInterval))
}
func deadmanDuration(d time.Duration) time.Duration {

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
}

View File

@ -5,10 +5,12 @@ import (
"fmt"
"io"
"net/http"
stdurl "net/url"
"reflect"
"time"
"github.com/gorilla/websocket"
"github.com/nlopes/slack/internal/timex"
)
// ManageConnection can be called on a Slack RTM instance returned by the
@ -37,6 +39,7 @@ func (rtm *RTM) ManageConnection() {
if info, conn, err = rtm.connect(connectionCount, rtm.useRTMStart); err != nil {
// when the connection is unsuccessful its fatal, and we need to bail out.
rtm.Debugf("Failed to connect with RTM on try %d: %s", connectionCount, err)
rtm.disconnect()
return
}
@ -44,7 +47,6 @@ func (rtm *RTM) ManageConnection() {
// and conn.
rtm.mu.Lock()
rtm.conn = conn
rtm.isConnected = true
rtm.info = info
rtm.mu.Unlock()
@ -55,20 +57,19 @@ func (rtm *RTM) ManageConnection() {
rtm.Debugf("RTM connection succeeded on try %d", connectionCount)
keepRunning := make(chan bool)
// we're now connected (or have failed fatally) so we can set up
// listeners
go rtm.handleIncomingEvents(keepRunning)
// we're now connected so we can set up listeners
go rtm.handleIncomingEvents()
// this should be a blocking call until the connection has ended
rtm.handleEvents(keepRunning)
rtm.handleEvents()
// after being disconnected we need to check if it was intentional
// if not then we should try to reconnect
if rtm.wasIntentional {
select {
case <-rtm.disconnected:
// after handle events returns we need to check if we're disconnected
return
default:
// otherwise continue and run the loop again to reconnect
}
// else continue and run the loop again to connect
}
}
@ -87,18 +88,20 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke
// used to provide exponential backoff wait time with jitter before trying
// to connect to slack again
boff := &backoff{
Min: 100 * time.Millisecond,
Max: 5 * time.Minute,
Factor: 2,
Jitter: true,
Max: 5 * time.Minute,
}
for {
var (
backoff time.Duration
)
// send connecting event
rtm.IncomingEvents <- RTMEvent{"connecting", &ConnectingEvent{
Attempt: boff.attempts + 1,
ConnectionCount: connectionCount,
}}
// attempt to start the connection
info, conn, err := rtm.startRTMAndDial(useRTMStart)
if err == nil {
@ -108,32 +111,48 @@ 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 intentional := <-rtm.killChannel:
if intentional {
rtm.killConnection(intentional)
return nil, nil, ErrRTMDisconnected
}
case <-rtm.disconnected:
rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: true}}
return nil, nil, fmt.Errorf("disconnect received while trying to connect")
return nil, nil, ErrRTMDisconnected
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)
time.Sleep(backoff)
}
}
@ -157,6 +176,14 @@ func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn
return nil, nil, err
}
// install connection parameters
u, err := stdurl.Parse(url)
if err != nil {
return nil, nil, err
}
u.RawQuery = rtm.connParams.Encode()
url = u.String()
rtm.Debugf("Dialing to websocket on url %s", url)
// Only use HTTPS for connections to prevent MITM attacks on the connection.
upgradeHeader := http.Header{}
@ -178,15 +205,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) (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}}
if intentional {
rtm.disconnect()
}
return err
}
@ -195,31 +226,29 @@ 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)
return
// detect when the connection is dead.
case <-rtm.pingDeadman.C:
rtm.Debugln("deadman switch trigger disconnecting")
_ = rtm.killConnection(keepRunning, false)
_ = rtm.killConnection(false)
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)
return
}
case <-rtm.forcePing:
err := rtm.ping()
if err != nil {
_ = rtm.killConnection(keepRunning, false)
if err := rtm.ping(); err != nil {
_ = rtm.killConnection(false)
return
}
// listen for messages that need to be sent
@ -229,7 +258,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)
return
default:
}
}
@ -241,17 +271,10 @@ func (rtm *RTM) handleEvents(keepRunning chan bool) {
//
// This will stop executing once the RTM's keepRunning channel has been closed
// or has anything sent to it.
func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) {
func (rtm *RTM) handleIncomingEvents() {
for {
// non-blocking listen to see if channel is closed
select {
// catch "stop" signal on channel close
case <-keepRunning:
if err := rtm.receiveIncomingEvent(); err != nil {
return
default:
if err := rtm.receiveIncomingEvent(); err != nil {
return
}
}
}
}
@ -274,7 +297,7 @@ func (rtm *RTM) sendWithDeadline(msg interface{}) error {
// and instead lets a future failed 'PING' detect the failed connection.
func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) {
rtm.Debugln("Sending message:", msg)
if len(msg.Text) > MaxMessageTextLength {
if len([]rune(msg.Text)) > MaxMessageTextLength {
rtm.IncomingEvents <- RTMEvent{"outgoing_error", &MessageTooLongEvent{
Message: msg,
MaxLength: MaxMessageTextLength,
@ -323,20 +346,32 @@ func (rtm *RTM) receiveIncomingEvent() error {
// 'PING' message
// trigger a 'PING' to detect potential websocket disconnect
rtm.forcePing <- true
select {
case rtm.forcePing <- true:
case <-rtm.disconnected:
}
case err != nil:
// All other errors from ReadJSON come from NextReader, and should
// kill the read loop and force a reconnect.
rtm.IncomingEvents <- RTMEvent{"incoming_error", &IncomingEventError{
ErrorObj: err,
}}
rtm.killChannel <- false
select {
case rtm.killChannel <- false:
case <-rtm.disconnected:
}
return err
case len(event) == 0:
rtm.Debugln("Received empty event")
default:
rtm.Debugln("Incoming Event:", string(event[:]))
rtm.rawEvents <- event
rtm.Debugln("Incoming Event:", string(event))
select {
case rtm.rawEvents <- event:
case <-rtm.disconnected:
rtm.Debugln("disonnected while attempting to send raw event")
}
}
return nil
}
@ -405,8 +440,7 @@ func (rtm *RTM) handlePong(event json.RawMessage) {
rtm.resetDeadman()
if err := json.Unmarshal(event, &p); err != nil {
logger.Println("RTM Error unmarshalling 'pong' event:", err)
rtm.Debugln(" -> Erroneous 'ping' event:", string(event))
rtm.Client.log.Println("RTM Error unmarshalling 'pong' event:", err)
return
}
@ -423,8 +457,8 @@ func (rtm *RTM) handlePong(event json.RawMessage) {
func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) {
v, exists := EventMapping[typeStr]
if !exists {
rtm.Debugf("RTM Error, received unmapped event %q: %s\n", typeStr, string(event))
err := fmt.Errorf("RTM Error: Received unmapped event %q: %s\n", typeStr, string(event))
rtm.Debugf("RTM Error - received unmapped event %q: %s\n", typeStr, string(event))
err := fmt.Errorf("RTM Error: Received unmapped event %q: %s", typeStr, string(event))
rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}}
return
}
@ -433,7 +467,7 @@ func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) {
err := json.Unmarshal(event, recvEvent)
if err != nil {
rtm.Debugf("RTM Error, could not unmarshall event %q: %s\n", typeStr, string(event))
err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s\n", typeStr, string(event))
err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s", typeStr, string(event))
rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}}
return
}

View File

@ -43,9 +43,10 @@ type HelloEvent struct{}
// PresenceChangeEvent represents the presence change event
type PresenceChangeEvent struct {
Type string `json:"type"`
Presence string `json:"presence"`
User string `json:"user"`
Type string `json:"type"`
Presence string `json:"presence"`
User string `json:"user"`
Users []string `json:"users"`
}
// UserTypingEvent represents the user typing event

4
vendor/modules.txt vendored
View File

@ -66,8 +66,10 @@ github.com/nicksnyder/go-i18n/i18n
github.com/nicksnyder/go-i18n/i18n/bundle
github.com/nicksnyder/go-i18n/i18n/language
github.com/nicksnyder/go-i18n/i18n/translation
# github.com/nlopes/slack v0.4.0
# github.com/nlopes/slack v0.5.1-0.20190623232825-2891986e2a3e
github.com/nlopes/slack
github.com/nlopes/slack/internal/errorsx
github.com/nlopes/slack/internal/timex
github.com/nlopes/slack/slackutilsx
# github.com/olivere/elastic v6.2.21+incompatible
github.com/olivere/elastic