Update nlopes/slack vendor

This commit is contained in:
Wim 2018-08-10 00:38:19 +02:00
parent 51062863a5
commit 68aeb93afa
57 changed files with 2654 additions and 2047 deletions

3
go.mod
View File

@ -41,13 +41,14 @@ require (
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect
github.com/nicksnyder/go-i18n v1.4.0 // indirect github.com/nicksnyder/go-i18n v1.4.0 // indirect
github.com/nlopes/slack v0.0.0-20180101221843-107290b5bbaf github.com/nlopes/slack v0.3.1-0.20180805133408-21749ab136a8
github.com/onsi/ginkgo v1.6.0 // indirect github.com/onsi/ginkgo v1.6.0 // indirect
github.com/onsi/gomega v1.4.1 // indirect github.com/onsi/gomega v1.4.1 // indirect
github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83 github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83
github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 // indirect github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 // indirect
github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e // indirect github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e // indirect
github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271 github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271
github.com/pkg/errors v0.8.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a
github.com/russross/blackfriday v1.5.1 github.com/russross/blackfriday v1.5.1

4
go.sum
View File

@ -83,6 +83,8 @@ github.com/nicksnyder/go-i18n v1.4.0 h1:AgLl+Yq7kg5OYlzCgu9cKTZOyI4tD/NgukKqLqC8
github.com/nicksnyder/go-i18n v1.4.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= github.com/nicksnyder/go-i18n v1.4.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
github.com/nlopes/slack v0.0.0-20180101221843-107290b5bbaf h1:M+xGhDxie/MqC+tzs+3ZHBSY4Wsv+fEkrpIMCKy8PTg= github.com/nlopes/slack v0.0.0-20180101221843-107290b5bbaf h1:M+xGhDxie/MqC+tzs+3ZHBSY4Wsv+fEkrpIMCKy8PTg=
github.com/nlopes/slack v0.0.0-20180101221843-107290b5bbaf/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= github.com/nlopes/slack v0.0.0-20180101221843-107290b5bbaf/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
github.com/nlopes/slack v0.3.1-0.20180805133408-21749ab136a8 h1:PSy8NkmkyldLmPPnNNw7mwfQFOHDqOI6bINpJ+/KV7Y=
github.com/nlopes/slack v0.3.1-0.20180805133408-21749ab136a8/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U= github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U=
@ -95,6 +97,8 @@ github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e h1:ZW8599OjioQsm
github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271 h1:wQ9lVx75za6AT2kI0S9QID0uWuwTWnvcTfN+uw1F8vg= github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271 h1:wQ9lVx75za6AT2kI0S9QID0uWuwTWnvcTfN+uw1F8vg=
github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271/go.mod h1:G7LufuPajuIvdt9OitkNt2qh0mmvD4bfRgRM7bhDIOA= github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271/go.mod h1:G7LufuPajuIvdt9OitkNt2qh0mmvD4bfRgRM7bhDIOA=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a h1:UWKek6MK3K6/TpbsFcv+8rrO6rSc6KKSp2FbMOHWsq4= github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a h1:UWKek6MK3K6/TpbsFcv+8rrO6rSc6KKSp2FbMOHWsq4=

View File

@ -1,2 +1,3 @@
*.test *.test
*~ *~
.idea/

View File

@ -1,3 +1,16 @@
### v0.3.0 - July 30, 2018
full differences can be viewed using `git log --oneline --decorate --color v0.2.0..v0.3.0`
- slack events initial support added. (still considered experimental and undergoing changes, stability not promised)
- vendored depedencies using dep, ensure using up to date tooling before filing issues.
- RTM has improved its ability to identify dead connections and reconnect automatically (worth calling out in case it has unintended side effects).
- bug fixes (various timestamp handling, error handling, RTM locking, etc).
### v0.2.0 - Feb 10, 2018
Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against.
Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0)
### v0.1.0 - May 28, 2017 ### v0.1.0 - May 28, 2017
This is released before adding context support. This is released before adding context support.

33
vendor/github.com/nlopes/slack/Gopkg.lock generated vendored Normal file
View File

@ -0,0 +1,33 @@
# 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/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 = "888307bf47ee004aaaa4c45e6139929b4984f2253e48e382246bfb8c66f3cd65"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -7,19 +7,20 @@ This library supports most if not all of the `api.slack.com` REST
calls, as well as the Real-Time Messaging protocol over websocket, in calls, as well as the Real-Time Messaging protocol over websocket, in
a fully managed way. a fully managed way.
## Change log ## 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.1.0 - May 28, 2017 ### v0.2.0 - Feb 10, 2018
This is released before adding context support. Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against.
As the used context package is the one from Go 1.7 this will be the last
compatible with Go < 1.7.
Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0) Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0)
### CHANGELOG.md ### CHANGELOG.md
As of this version a [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 ## Installing
@ -79,6 +80,11 @@ func main() {
See https://github.com/nlopes/slack/blob/master/examples/websocket/websocket.go See https://github.com/nlopes/slack/blob/master/examples/websocket/websocket.go
## Minimal EventsAPI usage:
See https://github.com/nlopes/slack/blob/master/examples/eventsapi/events.go
## Contributing ## Contributing
You are more than welcome to contribute to this project. Fork and You are more than welcome to contribute to this project. Fork and

View File

@ -12,9 +12,9 @@ type adminResponse struct {
Error string `json:"error"` Error string `json:"error"`
} }
func adminRequest(ctx context.Context, method string, teamName string, values url.Values, debug bool) (*adminResponse, error) { func adminRequest(ctx context.Context, client HTTPRequester, method string, teamName string, values url.Values, debug bool) (*adminResponse, error) {
adminResponse := &adminResponse{} adminResponse := &adminResponse{}
err := parseAdminResponse(ctx, method, teamName, values, adminResponse, debug) err := parseAdminResponse(ctx, client, method, teamName, values, adminResponse, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -35,12 +35,12 @@ func (api *Client) DisableUser(teamName string, uid string) error {
func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid string) error { func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid string) error {
values := url.Values{ values := url.Values{
"user": {uid}, "user": {uid},
"token": {api.config.token}, "token": {api.token},
"set_active": {"true"}, "set_active": {"true"},
"_attempts": {"1"}, "_attempts": {"1"},
} }
_, err := adminRequest(ctx, "setInactive", teamName, values, api.debug) _, err := adminRequest(ctx, api.httpclient, "setInactive", teamName, values, api.debug)
if err != nil { if err != nil {
return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err) return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err)
} }
@ -61,12 +61,13 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi
"first_name": {firstName}, "first_name": {firstName},
"last_name": {lastName}, "last_name": {lastName},
"ultra_restricted": {"1"}, "ultra_restricted": {"1"},
"token": {api.config.token}, "token": {api.token},
"resend": {"true"},
"set_active": {"true"}, "set_active": {"true"},
"_attempts": {"1"}, "_attempts": {"1"},
} }
_, err := adminRequest(ctx, "invite", teamName, values, api.debug) _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug)
if err != nil { if err != nil {
return fmt.Errorf("Failed to invite single-channel guest: %s", err) return fmt.Errorf("Failed to invite single-channel guest: %s", err)
} }
@ -87,12 +88,13 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe
"first_name": {firstName}, "first_name": {firstName},
"last_name": {lastName}, "last_name": {lastName},
"restricted": {"1"}, "restricted": {"1"},
"token": {api.config.token}, "token": {api.token},
"resend": {"true"},
"set_active": {"true"}, "set_active": {"true"},
"_attempts": {"1"}, "_attempts": {"1"},
} }
_, err := adminRequest(ctx, "invite", teamName, values, api.debug) _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug)
if err != nil { if err != nil {
return fmt.Errorf("Failed to restricted account: %s", err) return fmt.Errorf("Failed to restricted account: %s", err)
} }
@ -111,12 +113,12 @@ func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName,
"email": {emailAddress}, "email": {emailAddress},
"first_name": {firstName}, "first_name": {firstName},
"last_name": {lastName}, "last_name": {lastName},
"token": {api.config.token}, "token": {api.token},
"set_active": {"true"}, "set_active": {"true"},
"_attempts": {"1"}, "_attempts": {"1"},
} }
_, err := adminRequest(ctx, "invite", teamName, values, api.debug) _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug)
if err != nil { if err != nil {
return fmt.Errorf("Failed to invite to team: %s", err) return fmt.Errorf("Failed to invite to team: %s", err)
} }
@ -133,12 +135,12 @@ func (api *Client) SetRegular(teamName, user string) error {
func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) error { func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) error {
values := url.Values{ values := url.Values{
"user": {user}, "user": {user},
"token": {api.config.token}, "token": {api.token},
"set_active": {"true"}, "set_active": {"true"},
"_attempts": {"1"}, "_attempts": {"1"},
} }
_, err := adminRequest(ctx, "setRegular", teamName, values, api.debug) _, err := adminRequest(ctx, api.httpclient, "setRegular", teamName, values, api.debug)
if err != nil { if err != nil {
return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err) return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err)
} }
@ -155,12 +157,12 @@ func (api *Client) SendSSOBindingEmail(teamName, user string) error {
func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, user string) error { func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, user string) error {
values := url.Values{ values := url.Values{
"user": {user}, "user": {user},
"token": {api.config.token}, "token": {api.token},
"set_active": {"true"}, "set_active": {"true"},
"_attempts": {"1"}, "_attempts": {"1"},
} }
_, err := adminRequest(ctx, "sendSSOBind", teamName, values, api.debug) _, err := adminRequest(ctx, api.httpclient, "sendSSOBind", teamName, values, api.debug)
if err != nil { if err != nil {
return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err) return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err)
} }
@ -178,12 +180,12 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid,
values := url.Values{ values := url.Values{
"user": {uid}, "user": {uid},
"channel": {channel}, "channel": {channel},
"token": {api.config.token}, "token": {api.token},
"set_active": {"true"}, "set_active": {"true"},
"_attempts": {"1"}, "_attempts": {"1"},
} }
_, err := adminRequest(ctx, "setUltraRestricted", teamName, values, api.debug) _, err := adminRequest(ctx, api.httpclient, "setUltraRestricted", teamName, values, api.debug)
if err != nil { if err != nil {
return fmt.Errorf("Failed to ultra-restrict account: %s", err) return fmt.Errorf("Failed to ultra-restrict account: %s", err)
} }
@ -200,12 +202,12 @@ func (api *Client) SetRestricted(teamName, uid string) error {
func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string) error { func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string) error {
values := url.Values{ values := url.Values{
"user": {uid}, "user": {uid},
"token": {api.config.token}, "token": {api.token},
"set_active": {"true"}, "set_active": {"true"},
"_attempts": {"1"}, "_attempts": {"1"},
} }
_, err := adminRequest(ctx, "setRestricted", teamName, values, api.debug) _, err := adminRequest(ctx, api.httpclient, "setRestricted", teamName, values, api.debug)
if err != nil { if err != nil {
return fmt.Errorf("Failed to restrict account: %s", err) return fmt.Errorf("Failed to restrict account: %s", err)
} }

View File

@ -59,6 +59,7 @@ type AttachmentActionCallback struct {
AttachmentID string `json:"attachment_id"` AttachmentID string `json:"attachment_id"`
Token string `json:"token"` Token string `json:"token"`
ResponseURL string `json:"response_url"` ResponseURL string `json:"response_url"`
TriggerID string `json:"trigger_id"`
} }
// ConfirmationField are used to ask users to confirm actions // ConfirmationField are used to ask users to confirm actions
@ -75,7 +76,9 @@ type Attachment struct {
Fallback string `json:"fallback"` Fallback string `json:"fallback"`
CallbackID string `json:"callback_id,omitempty"` CallbackID string `json:"callback_id,omitempty"`
ID int `json:"id,omitempty"`
AuthorID string `json:"author_id,omitempty"`
AuthorName string `json:"author_name,omitempty"` AuthorName string `json:"author_name,omitempty"`
AuthorSubname string `json:"author_subname,omitempty"` AuthorSubname string `json:"author_subname,omitempty"`
AuthorLink string `json:"author_link,omitempty"` AuthorLink string `json:"author_link,omitempty"`

View File

@ -38,7 +38,7 @@ func (b *backoff) Duration() time.Duration {
} }
//calculate this duration //calculate this duration
dur := float64(b.Min) * math.Pow(b.Factor, float64(b.attempts)) dur := float64(b.Min) * math.Pow(b.Factor, float64(b.attempts))
if b.Jitter == true { if b.Jitter {
dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min) dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min)
} }
//cap! //cap!

View File

@ -19,9 +19,9 @@ type botResponseFull struct {
SlackResponse SlackResponse
} }
func botRequest(ctx context.Context, path string, values url.Values, debug bool) (*botResponseFull, error) { func botRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*botResponseFull, error) {
response := &botResponseFull{} response := &botResponseFull{}
err := post(ctx, path, values, response, debug) err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -39,10 +39,11 @@ func (api *Client) GetBotInfo(bot string) (*Bot, error) {
// GetBotInfoContext will retrieve the complete bot information using a custom context // GetBotInfoContext will retrieve the complete bot information using a custom context
func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) { func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"bot": {bot}, "bot": {bot},
} }
response, err := botRequest(ctx, "bots.info", values, api.debug)
response, err := botRequest(ctx, api.httpclient, "bots.info", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -20,14 +20,15 @@ type channelResponseFull struct {
// Channel contains information about the channel // Channel contains information about the channel
type Channel struct { type Channel struct {
groupConversation groupConversation
IsChannel bool `json:"is_channel"` IsChannel bool `json:"is_channel"`
IsGeneral bool `json:"is_general"` IsGeneral bool `json:"is_general"`
IsMember bool `json:"is_member"` IsMember bool `json:"is_member"`
Locale string `json:"locale"`
} }
func channelRequest(ctx context.Context, path string, values url.Values, debug bool) (*channelResponseFull, error) { func channelRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*channelResponseFull, error) {
response := &channelResponseFull{} response := &channelResponseFull{}
err := post(ctx, path, values, response, debug) err := postForm(ctx, client, SLACK_API+path, values, response, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -45,12 +46,13 @@ func (api *Client) ArchiveChannel(channelID string) error {
// ArchiveChannelContext archives the given channel with a custom context // ArchiveChannelContext archives the given channel with a custom context
// see https://api.slack.com/methods/channels.archive // see https://api.slack.com/methods/channels.archive
func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) error { func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) (err error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
} }
_, err := channelRequest(ctx, "channels.archive", values, api.debug)
_, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug)
return err return err
} }
@ -62,12 +64,13 @@ func (api *Client) UnarchiveChannel(channelID string) error {
// UnarchiveChannelContext unarchives the given channel with a custom context // UnarchiveChannelContext unarchives the given channel with a custom context
// see https://api.slack.com/methods/channels.unarchive // see https://api.slack.com/methods/channels.unarchive
func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string) error { func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string) (err error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
} }
_, err := channelRequest(ctx, "channels.unarchive", values, api.debug)
_, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug)
return err return err
} }
@ -81,10 +84,11 @@ func (api *Client) CreateChannel(channelName string) (*Channel, error) {
// see https://api.slack.com/methods/channels.create // see https://api.slack.com/methods/channels.create
func (api *Client) CreateChannelContext(ctx context.Context, channelName string) (*Channel, error) { func (api *Client) CreateChannelContext(ctx context.Context, channelName string) (*Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"name": {channelName}, "name": {channelName},
} }
response, err := channelRequest(ctx, "channels.create", values, api.debug)
response, err := channelRequest(ctx, api.httpclient, "channels.create", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -101,7 +105,7 @@ func (api *Client) GetChannelHistory(channelID string, params HistoryParameters)
// see https://api.slack.com/methods/channels.history // see https://api.slack.com/methods/channels.history
func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID string, params HistoryParameters) (*History, error) { func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID string, params HistoryParameters) (*History, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
} }
if params.Latest != DEFAULT_HISTORY_LATEST { if params.Latest != DEFAULT_HISTORY_LATEST {
@ -120,6 +124,7 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID strin
values.Add("inclusive", "0") values.Add("inclusive", "0")
} }
} }
if params.Unreads != DEFAULT_HISTORY_UNREADS { if params.Unreads != DEFAULT_HISTORY_UNREADS {
if params.Unreads { if params.Unreads {
values.Add("unreads", "1") values.Add("unreads", "1")
@ -127,7 +132,8 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID strin
values.Add("unreads", "0") values.Add("unreads", "0")
} }
} }
response, err := channelRequest(ctx, "channels.history", values, api.debug)
response, err := channelRequest(ctx, api.httpclient, "channels.history", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -144,10 +150,11 @@ func (api *Client) GetChannelInfo(channelID string) (*Channel, error) {
// see https://api.slack.com/methods/channels.info // see https://api.slack.com/methods/channels.info
func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) { func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
} }
response, err := channelRequest(ctx, "channels.info", values, api.debug)
response, err := channelRequest(ctx, api.httpclient, "channels.info", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -164,11 +171,12 @@ func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error)
// see https://api.slack.com/methods/channels.invite // see https://api.slack.com/methods/channels.invite
func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) { func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
"user": {user}, "user": {user},
} }
response, err := channelRequest(ctx, "channels.invite", values, api.debug)
response, err := channelRequest(ctx, api.httpclient, "channels.invite", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -185,10 +193,11 @@ func (api *Client) JoinChannel(channelName string) (*Channel, error) {
// see https://api.slack.com/methods/channels.join // see https://api.slack.com/methods/channels.join
func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (*Channel, error) { func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (*Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"name": {channelName}, "name": {channelName},
} }
response, err := channelRequest(ctx, "channels.join", values, api.debug)
response, err := channelRequest(ctx, api.httpclient, "channels.join", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -205,17 +214,16 @@ func (api *Client) LeaveChannel(channelID string) (bool, error) {
// see https://api.slack.com/methods/channels.leave // see https://api.slack.com/methods/channels.leave
func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (bool, error) { func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (bool, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
} }
response, err := channelRequest(ctx, "channels.leave", values, api.debug)
response, err := channelRequest(ctx, api.httpclient, "channels.leave", values, api.debug)
if err != nil { if err != nil {
return false, err return false, err
} }
if response.NotInChannel {
return response.NotInChannel, nil return response.NotInChannel, nil
}
return false, nil
} }
// KickUserFromChannel kicks a user from a given channel // KickUserFromChannel kicks a user from a given channel
@ -226,13 +234,14 @@ func (api *Client) KickUserFromChannel(channelID, user string) error {
// KickUserFromChannelContext kicks a user from a given channel with a custom context // KickUserFromChannelContext kicks a user from a given channel with a custom context
// see https://api.slack.com/methods/channels.kick // see https://api.slack.com/methods/channels.kick
func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, user string) error { func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, user string) (err error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
"user": {user}, "user": {user},
} }
_, err := channelRequest(ctx, "channels.kick", values, api.debug)
_, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug)
return err return err
} }
@ -246,12 +255,13 @@ func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) {
// see https://api.slack.com/methods/channels.list // see https://api.slack.com/methods/channels.list
func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) ([]Channel, error) { func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) ([]Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
if excludeArchived { if excludeArchived {
values.Add("exclude_archived", "1") values.Add("exclude_archived", "1")
} }
response, err := channelRequest(ctx, "channels.list", values, api.debug)
response, err := channelRequest(ctx, api.httpclient, "channels.list", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -271,13 +281,14 @@ func (api *Client) SetChannelReadMark(channelID, ts string) error {
// SetChannelReadMarkContext sets the read mark of a given channel to a specific point with a custom context // SetChannelReadMarkContext sets the read mark of a given channel to a specific point with a custom context
// For more details see SetChannelReadMark documentation // For more details see SetChannelReadMark documentation
// see https://api.slack.com/methods/channels.mark // see https://api.slack.com/methods/channels.mark
func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts string) error { func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts string) (err error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
"ts": {ts}, "ts": {ts},
} }
_, err := channelRequest(ctx, "channels.mark", values, api.debug)
_, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug)
return err return err
} }
@ -291,13 +302,14 @@ func (api *Client) RenameChannel(channelID, name string) (*Channel, error) {
// see https://api.slack.com/methods/channels.rename // see https://api.slack.com/methods/channels.rename
func (api *Client) RenameChannelContext(ctx context.Context, channelID, name string) (*Channel, error) { func (api *Client) RenameChannelContext(ctx context.Context, channelID, name string) (*Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
"name": {name}, "name": {name},
} }
// XXX: the created entry in this call returns a string instead of a number // 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. // so I may have to do some workaround to solve it.
response, err := channelRequest(ctx, "channels.rename", values, api.debug) response, err := channelRequest(ctx, api.httpclient, "channels.rename", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -314,11 +326,12 @@ func (api *Client) SetChannelPurpose(channelID, purpose string) (string, error)
// see https://api.slack.com/methods/channels.setPurpose // see https://api.slack.com/methods/channels.setPurpose
func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purpose string) (string, error) { func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purpose string) (string, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
"purpose": {purpose}, "purpose": {purpose},
} }
response, err := channelRequest(ctx, "channels.setPurpose", values, api.debug)
response, err := channelRequest(ctx, api.httpclient, "channels.setPurpose", values, api.debug)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -335,11 +348,12 @@ func (api *Client) SetChannelTopic(channelID, topic string) (string, error) {
// see https://api.slack.com/methods/channels.setTopic // see https://api.slack.com/methods/channels.setTopic
func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic string) (string, error) { func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic string) (string, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
"topic": {topic}, "topic": {topic},
} }
response, err := channelRequest(ctx, "channels.setTopic", values, api.debug)
response, err := channelRequest(ctx, api.httpclient, "channels.setTopic", values, api.debug)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -356,11 +370,11 @@ func (api *Client) GetChannelReplies(channelID, thread_ts string) ([]Message, er
// see https://api.slack.com/methods/channels.replies // see https://api.slack.com/methods/channels.replies
func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) { func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channelID}, "channel": {channelID},
"thread_ts": {thread_ts}, "thread_ts": {thread_ts},
} }
response, err := channelRequest(ctx, "channels.replies", values, api.debug) response, err := channelRequest(ctx, api.httpclient, "channels.replies", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -3,17 +3,16 @@ package slack
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"net/url" "net/url"
"strings" "strings"
) )
const ( const (
DEFAULT_MESSAGE_USERNAME = "" DEFAULT_MESSAGE_USERNAME = ""
DEFAULT_MESSAGE_THREAD_TIMESTAMP = ""
DEFAULT_MESSAGE_REPLY_BROADCAST = false DEFAULT_MESSAGE_REPLY_BROADCAST = false
DEFAULT_MESSAGE_ASUSER = false DEFAULT_MESSAGE_ASUSER = false
DEFAULT_MESSAGE_PARSE = "" DEFAULT_MESSAGE_PARSE = ""
DEFAULT_MESSAGE_THREAD_TIMESTAMP = ""
DEFAULT_MESSAGE_LINK_NAMES = 0 DEFAULT_MESSAGE_LINK_NAMES = 0
DEFAULT_MESSAGE_UNFURL_LINKS = false DEFAULT_MESSAGE_UNFURL_LINKS = false
DEFAULT_MESSAGE_UNFURL_MEDIA = true DEFAULT_MESSAGE_UNFURL_MEDIA = true
@ -24,16 +23,26 @@ const (
) )
type chatResponseFull struct { type chatResponseFull struct {
Channel string `json:"channel"` Channel string `json:"channel"`
Timestamp string `json:"ts"` Timestamp string `json:"ts"` //Regualr message timestamp
Text string `json:"text"` MessageTimeStamp string `json:"message_ts"` //Ephemeral message timestamp
Text string `json:"text"`
SlackResponse SlackResponse
} }
// getMessageTimestamp will inspect the `chatResponseFull` to ruturn a timestamp value
// in `chat.postMessage` its under `ts`
// in `chat.postEphemeral` its under `message_ts`
func (c chatResponseFull) getMessageTimestamp() string {
if len(c.Timestamp) > 0 {
return c.Timestamp
}
return c.MessageTimeStamp
}
// PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request // PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request
type PostMessageParameters struct { type PostMessageParameters struct {
Text string `json:"text"` Username string `json:"username"`
Username string `json:"user_name"`
AsUser bool `json:"as_user"` AsUser bool `json:"as_user"`
Parse string `json:"parse"` Parse string `json:"parse"`
ThreadTimestamp string `json:"thread_ts"` ThreadTimestamp string `json:"thread_ts"`
@ -55,18 +64,19 @@ type PostMessageParameters struct {
// NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set // NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set
func NewPostMessageParameters() PostMessageParameters { func NewPostMessageParameters() PostMessageParameters {
return PostMessageParameters{ return PostMessageParameters{
Username: DEFAULT_MESSAGE_USERNAME, Username: DEFAULT_MESSAGE_USERNAME,
User: DEFAULT_MESSAGE_USERNAME, User: DEFAULT_MESSAGE_USERNAME,
AsUser: DEFAULT_MESSAGE_ASUSER, AsUser: DEFAULT_MESSAGE_ASUSER,
Parse: DEFAULT_MESSAGE_PARSE, Parse: DEFAULT_MESSAGE_PARSE,
LinkNames: DEFAULT_MESSAGE_LINK_NAMES, ThreadTimestamp: DEFAULT_MESSAGE_THREAD_TIMESTAMP,
Attachments: nil, LinkNames: DEFAULT_MESSAGE_LINK_NAMES,
UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS, Attachments: nil,
UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA, UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS,
IconURL: DEFAULT_MESSAGE_ICON_URL, UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA,
IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI, IconURL: DEFAULT_MESSAGE_ICON_URL,
Markdown: DEFAULT_MESSAGE_MARKDOWN, IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI,
EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT, Markdown: DEFAULT_MESSAGE_MARKDOWN,
EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT,
} }
} }
@ -112,11 +122,10 @@ func (api *Client) PostMessageContext(ctx context.Context, channel, text string,
// PostEphemeral sends an ephemeral message to a user in a channel. // PostEphemeral sends an ephemeral message to a user in a channel.
// Message is escaped by default according to https://api.slack.com/docs/formatting // 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. // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message.
func (api *Client) PostEphemeral(channel, userID string, options ...MsgOption) (string, error) { func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) (string, error) {
options = append(options, MsgOptionPostEphemeral())
return api.PostEphemeralContext( return api.PostEphemeralContext(
context.Background(), context.Background(),
channel, channelID,
userID, userID,
options..., options...,
) )
@ -124,30 +133,19 @@ func (api *Client) PostEphemeral(channel, userID string, options ...MsgOption) (
// PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context // PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context
// For more details, see PostEphemeral documentation // For more details, see PostEphemeral documentation
func (api *Client) PostEphemeralContext(ctx context.Context, channel, userID string, options ...MsgOption) (string, error) { func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID string, options ...MsgOption) (timestamp string, err error) {
path, values, err := ApplyMsgOptions(api.config.token, channel, options...) _, timestamp, _, err = api.SendMessageContext(ctx, channelID, append(options, MsgOptionPostEphemeral2(userID))...)
if err != nil { return timestamp, err
return "", err
}
values.Add("user", userID)
response, err := chatRequest(ctx, path, values, api.debug)
if err != nil {
return "", err
}
return response.Timestamp, nil
} }
// UpdateMessage updates a message in a channel // UpdateMessage updates a message in a channel
func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) { func (api *Client) UpdateMessage(channelID, timestamp, text string) (string, string, string, error) {
return api.UpdateMessageContext(context.Background(), channel, timestamp, text) return api.UpdateMessageContext(context.Background(), channelID, timestamp, text)
} }
// UpdateMessage updates a message in a channel // UpdateMessageContext updates a message in a channel
func (api *Client) UpdateMessageContext(ctx context.Context, channel, timestamp, text string) (string, string, string, error) { func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp, text string) (string, string, string, error) {
return api.SendMessageContext(ctx, channel, MsgOptionUpdate(timestamp), MsgOptionText(text, true)) return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionText(text, true))
} }
// SendMessage more flexible method for configuring messages. // SendMessage more flexible method for configuring messages.
@ -156,22 +154,30 @@ func (api *Client) SendMessage(channel string, options ...MsgOption) (string, st
} }
// SendMessageContext more flexible method for configuring messages with a custom context. // SendMessageContext more flexible method for configuring messages with a custom context.
func (api *Client) SendMessageContext(ctx context.Context, channel string, options ...MsgOption) (string, string, string, error) { func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (channel string, timestamp string, text string, err error) {
channel, values, err := ApplyMsgOptions(api.config.token, channel, options...) var (
if err != nil { config sendConfig
response chatResponseFull
)
if config, err = applyMsgOptions(api.token, channelID, options...); err != nil {
return "", "", "", err return "", "", "", err
} }
response, err := chatRequest(ctx, channel, values, api.debug) if err = postSlackMethod(ctx, api.httpclient, string(config.mode), config.values, &response, api.debug); err != nil {
if err != nil {
return "", "", "", err return "", "", "", err
} }
return response.Channel, response.Timestamp, response.Text, nil return response.Channel, response.getMessageTimestamp(), response.Text, response.Err()
} }
// ApplyMsgOptions utility function for debugging/testing chat requests. // ApplyMsgOptions utility function for debugging/testing chat requests.
func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) { func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) {
config, err := applyMsgOptions(token, channel, options...)
return string(config.mode), config.values, err
}
func applyMsgOptions(token, channel string, options ...MsgOption) (sendConfig, error) {
config := sendConfig{ config := sendConfig{
mode: chatPostMessage, mode: chatPostMessage,
values: url.Values{ values: url.Values{
@ -182,11 +188,11 @@ func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.V
for _, opt := range options { for _, opt := range options {
if err := opt(&config); err != nil { if err := opt(&config); err != nil {
return string(config.mode), config.values, err return config, err
} }
} }
return string(config.mode), config.values, nil return config, nil
} }
func escapeMessage(message string) string { func escapeMessage(message string) string {
@ -194,18 +200,6 @@ func escapeMessage(message string) string {
return replacer.Replace(message) return replacer.Replace(message)
} }
func chatRequest(ctx context.Context, path string, values url.Values, debug bool) (*chatResponseFull, error) {
response := &chatResponseFull{}
err := post(ctx, path, values, response, debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response, nil
}
type sendMode string type sendMode string
const ( const (
@ -213,6 +207,7 @@ const (
chatPostMessage sendMode = "chat.postMessage" chatPostMessage sendMode = "chat.postMessage"
chatDelete sendMode = "chat.delete" chatDelete sendMode = "chat.delete"
chatPostEphemeral sendMode = "chat.postEphemeral" chatPostEphemeral sendMode = "chat.postEphemeral"
chatMeMessage sendMode = "chat.meMessage"
) )
type sendConfig struct { type sendConfig struct {
@ -232,7 +227,8 @@ func MsgOptionPost() MsgOption {
} }
} }
// MsgOptionPostEphemeral posts an ephemeral message // MsgOptionPostEphemeral - DEPRECATED: use MsgOptionPostEphemeral2
// posts an ephemeral message.
func MsgOptionPostEphemeral() MsgOption { func MsgOptionPostEphemeral() MsgOption {
return func(config *sendConfig) error { return func(config *sendConfig) error {
config.mode = chatPostEphemeral config.mode = chatPostEphemeral
@ -241,6 +237,25 @@ func MsgOptionPostEphemeral() MsgOption {
} }
} }
// MsgOptionPostEphemeral2 - posts an ephemeral message to the provided user.
func MsgOptionPostEphemeral2(userID string) MsgOption {
return func(config *sendConfig) error {
config.mode = chatPostEphemeral
MsgOptionUser(userID)(config)
config.values.Del("ts")
return nil
}
}
// MsgOptionMeMessage posts a "me message" type from the calling user
func MsgOptionMeMessage() MsgOption {
return func(config *sendConfig) error {
config.mode = chatMeMessage
return nil
}
}
// MsgOptionUpdate updates a message based on the timestamp. // MsgOptionUpdate updates a message based on the timestamp.
func MsgOptionUpdate(timestamp string) MsgOption { func MsgOptionUpdate(timestamp string) MsgOption {
return func(config *sendConfig) error { return func(config *sendConfig) error {
@ -269,6 +284,14 @@ func MsgOptionAsUser(b bool) MsgOption {
} }
} }
// MsgOptionUser set the user for the message.
func MsgOptionUser(userID string) MsgOption {
return func(config *sendConfig) error {
config.values.Set("user", userID)
return nil
}
}
// MsgOptionText provide the text for the message, optionally escape the provided // MsgOptionText provide the text for the message, optionally escape the provided
// text. // text.
func MsgOptionText(text string, escape bool) MsgOption { func MsgOptionText(text string, escape bool) MsgOption {
@ -304,6 +327,14 @@ func MsgOptionEnableLinkUnfurl() MsgOption {
} }
} }
// MsgOptionDisableLinkUnfurl disables link unfurling
func MsgOptionDisableLinkUnfurl() MsgOption {
return func(config *sendConfig) error {
config.values.Set("unfurl_links", "false")
return nil
}
}
// MsgOptionDisableMediaUnfurl disables media unfurling. // MsgOptionDisableMediaUnfurl disables media unfurling.
func MsgOptionDisableMediaUnfurl() MsgOption { func MsgOptionDisableMediaUnfurl() MsgOption {
return func(config *sendConfig) error { return func(config *sendConfig) error {
@ -320,11 +351,52 @@ func MsgOptionDisableMarkdown() MsgOption {
} }
} }
// MsgOptionTS sets the thread TS of the message to enable creating or replying to a thread
func MsgOptionTS(ts string) MsgOption {
return func(config *sendConfig) error {
config.values.Set("thread_ts", ts)
return nil
}
}
// MsgOptionBroadcast sets reply_broadcast to true
func MsgOptionBroadcast() MsgOption {
return func(config *sendConfig) error {
config.values.Set("reply_broadcast", "true")
return nil
}
}
// this function combines multiple options into a single option.
func MsgOptionCompose(options ...MsgOption) MsgOption {
return func(c *sendConfig) error {
for _, opt := range options {
if err := opt(c); err != nil {
return err
}
}
return nil
}
}
func MsgOptionParse(b bool) MsgOption {
return func(c *sendConfig) error {
var v string
if b {
v = "1"
} else {
v = "0"
}
c.values.Set("parse", v)
return nil
}
}
// MsgOptionPostMessageParameters maintain backwards compatibility. // MsgOptionPostMessageParameters maintain backwards compatibility.
func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
return func(config *sendConfig) error { return func(config *sendConfig) error {
if params.Username != DEFAULT_MESSAGE_USERNAME { if params.Username != DEFAULT_MESSAGE_USERNAME {
config.values.Set("username", string(params.Username)) config.values.Set("username", params.Username)
} }
// chat.postEphemeral support // chat.postEphemeral support
@ -336,7 +408,7 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
MsgOptionAsUser(params.AsUser)(config) MsgOptionAsUser(params.AsUser)(config)
if params.Parse != DEFAULT_MESSAGE_PARSE { if params.Parse != DEFAULT_MESSAGE_PARSE {
config.values.Set("parse", string(params.Parse)) config.values.Set("parse", params.Parse)
} }
if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES {
config.values.Set("link_names", "1") config.values.Set("link_names", "1")

View File

@ -1,5 +1,13 @@
package slack package slack
import (
"context"
"errors"
"net/url"
"strconv"
"strings"
)
// Conversation is the foundation for IM and BaseGroupConversation // Conversation is the foundation for IM and BaseGroupConversation
type conversation struct { type conversation struct {
ID string `json:"id"` ID string `json:"id"`
@ -9,6 +17,20 @@ type conversation struct {
Latest *Message `json:"latest,omitempty"` Latest *Message `json:"latest,omitempty"`
UnreadCount int `json:"unread_count,omitempty"` UnreadCount int `json:"unread_count,omitempty"`
UnreadCountDisplay int `json:"unread_count_display,omitempty"` UnreadCountDisplay int `json:"unread_count_display,omitempty"`
IsGroup bool `json:"is_group"`
IsShared bool `json:"is_shared"`
IsIM bool `json:"is_im"`
IsExtShared bool `json:"is_ext_shared"`
IsOrgShared bool `json:"is_org_shared"`
IsPendingExtShared bool `json:"is_pending_ext_shared"`
IsPrivate bool `json:"is_private"`
IsMpIM bool `json:"is_mpim"`
Unlinked int `json:"unlinked"`
NameNormalized string `json:"name_normalized"`
NumMembers int `json:"num_members"`
Priority float64 `json:"priority"`
// TODO support pending_shared
// TODO support previous_names
} }
// GroupConversation is the foundation for Group and Channel // GroupConversation is the foundation for Group and Channel
@ -35,3 +57,510 @@ type Purpose struct {
Creator string `json:"creator"` Creator string `json:"creator"`
LastSet JSONTime `json:"last_set"` LastSet JSONTime `json:"last_set"`
} }
type GetUsersInConversationParameters struct {
ChannelID string
Cursor string
Limit int
}
type responseMetaData struct {
NextCursor string `json:"next_cursor"`
}
// GetUsersInConversation returns the list of users in a conversation
func (api *Client) GetUsersInConversation(params *GetUsersInConversationParameters) ([]string, string, error) {
return api.GetUsersInConversationContext(context.Background(), params)
}
// GetUsersInConversationContext returns the list of users in a conversation with a custom context
func (api *Client) GetUsersInConversationContext(ctx context.Context, params *GetUsersInConversationParameters) ([]string, string, error) {
values := url.Values{
"token": {api.token},
"channel": {params.ChannelID},
}
if params.Cursor != "" {
values.Add("cursor", params.Cursor)
}
if params.Limit != 0 {
values.Add("limit", strconv.Itoa(params.Limit))
}
response := struct {
Members []string `json:"members"`
ResponseMetaData responseMetaData `json:"response_metadata"`
SlackResponse
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.members", values, &response, api.debug)
if err != nil {
return nil, "", err
}
if !response.Ok {
return nil, "", errors.New(response.Error)
}
return response.Members, response.ResponseMetaData.NextCursor, nil
}
// ArchiveConversation archives a conversation
func (api *Client) ArchiveConversation(channelID string) error {
return api.ArchiveConversationContext(context.Background(), channelID)
}
// ArchiveConversationContext archives a conversation with a custom context
func (api *Client) ArchiveConversationContext(ctx context.Context, channelID string) error {
values := url.Values{
"token": {api.token},
"channel": {channelID},
}
response := SlackResponse{}
err := postSlackMethod(ctx, api.httpclient, "conversations.archive", values, &response, api.debug)
if err != nil {
return err
}
return response.Err()
}
// UnArchiveConversation reverses conversation archival
func (api *Client) UnArchiveConversation(channelID string) error {
return api.UnArchiveConversationContext(context.Background(), channelID)
}
// UnArchiveConversationContext reverses conversation archival with a custom context
func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID string) error {
values := url.Values{
"token": {api.token},
"channel": {channelID},
}
response := SlackResponse{}
err := postSlackMethod(ctx, api.httpclient, "conversations.unarchive", values, &response, api.debug)
if err != nil {
return err
}
return response.Err()
}
// SetTopicOfConversation sets the topic for a conversation
func (api *Client) SetTopicOfConversation(channelID, topic string) (*Channel, error) {
return api.SetTopicOfConversationContext(context.Background(), channelID, topic)
}
// SetTopicOfConversationContext sets the topic for a conversation with a custom context
func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID, topic string) (*Channel, error) {
values := url.Values{
"token": {api.token},
"channel": {channelID},
"topic": {topic},
}
response := struct {
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.setTopic", values, &response, api.debug)
if err != nil {
return nil, err
}
return response.Channel, response.Err()
}
// SetPurposeOfConversation sets the purpose for a conversation
func (api *Client) SetPurposeOfConversation(channelID, purpose string) (*Channel, error) {
return api.SetPurposeOfConversationContext(context.Background(), channelID, purpose)
}
// SetPurposeOfConversationContext sets the purpose for a conversation with a custom context
func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelID, purpose string) (*Channel, error) {
values := url.Values{
"token": {api.token},
"channel": {channelID},
"purpose": {purpose},
}
response := struct {
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.setPurpose", values, &response, api.debug)
if err != nil {
return nil, err
}
return response.Channel, response.Err()
}
// RenameConversation renames a conversation
func (api *Client) RenameConversation(channelID, channelName string) (*Channel, error) {
return api.RenameConversationContext(context.Background(), channelID, channelName)
}
// RenameConversationContext renames a conversation with a custom context
func (api *Client) RenameConversationContext(ctx context.Context, channelID, channelName string) (*Channel, error) {
values := url.Values{
"token": {api.token},
"channel": {channelID},
"name": {channelName},
}
response := struct {
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.rename", values, &response, api.debug)
if err != nil {
return nil, err
}
return response.Channel, response.Err()
}
// InviteUsersToConversation invites users to a channel
func (api *Client) InviteUsersToConversation(channelID string, users ...string) (*Channel, error) {
return api.InviteUsersToConversationContext(context.Background(), channelID, users...)
}
// InviteUsersToConversationContext invites users to a channel with a custom context
func (api *Client) InviteUsersToConversationContext(ctx context.Context, channelID string, users ...string) (*Channel, error) {
values := url.Values{
"token": {api.token},
"channel": {channelID},
"users": {strings.Join(users, ",")},
}
response := struct {
SlackResponse
Channel *Channel `json:"channel"`
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.invite", values, &response, api.debug)
if err != nil {
return nil, err
}
return response.Channel, response.Err()
}
// KickUserFromConversation removes a user from a conversation
func (api *Client) KickUserFromConversation(channelID string, user string) error {
return api.KickUserFromConversationContext(context.Background(), channelID, user)
}
// KickUserFromConversationContext removes a user from a conversation with a custom context
func (api *Client) KickUserFromConversationContext(ctx context.Context, channelID string, user string) error {
values := url.Values{
"token": {api.token},
"channel": {channelID},
"user": {user},
}
response := SlackResponse{}
err := postSlackMethod(ctx, api.httpclient, "conversations.kick", values, &response, api.debug)
if err != nil {
return err
}
return response.Err()
}
// CloseConversation closes a direct message or multi-person direct message
func (api *Client) CloseConversation(channelID string) (noOp bool, alreadyClosed bool, err error) {
return api.CloseConversationContext(context.Background(), channelID)
}
// CloseConversationContext closes a direct message or multi-person direct message with a custom context
func (api *Client) CloseConversationContext(ctx context.Context, channelID string) (noOp bool, alreadyClosed bool, err error) {
values := url.Values{
"token": {api.token},
"channel": {channelID},
}
response := struct {
SlackResponse
NoOp bool `json:"no_op"`
AlreadyClosed bool `json:"already_closed"`
}{}
err = postSlackMethod(ctx, api.httpclient, "conversations.close", values, &response, api.debug)
if err != nil {
return false, false, err
}
return response.NoOp, response.AlreadyClosed, response.Err()
}
// CreateConversation initiates a public or private channel-based conversation
func (api *Client) CreateConversation(channelName string, isPrivate bool) (*Channel, error) {
return api.CreateConversationContext(context.Background(), channelName, isPrivate)
}
// CreateConversationContext initiates a public or private channel-based conversation with a custom context
func (api *Client) CreateConversationContext(ctx context.Context, channelName string, isPrivate bool) (*Channel, error) {
values := url.Values{
"token": {api.token},
"name": {channelName},
"is_private": {strconv.FormatBool(isPrivate)},
}
response, err := channelRequest(
ctx, api.httpclient, "conversations.create", values, api.debug)
if err != nil {
return nil, err
}
return &response.Channel, response.Err()
}
// GetConversationInfo retrieves information about a conversation
func (api *Client) GetConversationInfo(channelID string, includeLocale bool) (*Channel, error) {
return api.GetConversationInfoContext(context.Background(), channelID, includeLocale)
}
// GetConversationInfoContext retrieves information about a conversation with a custom context
func (api *Client) GetConversationInfoContext(ctx context.Context, channelID string, includeLocale bool) (*Channel, error) {
values := url.Values{
"token": {api.token},
"channel": {channelID},
"include_locale": {strconv.FormatBool(includeLocale)},
}
response, err := channelRequest(
ctx, api.httpclient, "conversations.info", values, api.debug)
if err != nil {
return nil, err
}
return &response.Channel, response.Err()
}
// LeaveConversation leaves a conversation
func (api *Client) LeaveConversation(channelID string) (bool, error) {
return api.LeaveConversationContext(context.Background(), channelID)
}
// LeaveConversationContext leaves a conversation with a custom context
func (api *Client) LeaveConversationContext(ctx context.Context, channelID string) (bool, error) {
values := url.Values{
"token": {api.token},
"channel": {channelID},
}
response, err := channelRequest(ctx, api.httpclient, "conversations.leave", values, api.debug)
if err != nil {
return false, err
}
return response.NotInChannel, err
}
type GetConversationRepliesParameters struct {
ChannelID string
Timestamp string
Cursor string
Inclusive bool
Latest string
Limit int
Oldest string
}
// GetConversationReplies retrieves a thread of messages posted to a conversation
func (api *Client) GetConversationReplies(params *GetConversationRepliesParameters) (msgs []Message, hasMore bool, nextCursor string, err error) {
return api.GetConversationRepliesContext(context.Background(), params)
}
// GetConversationRepliesContext retrieves a thread of messages posted to a conversation with a custom context
func (api *Client) GetConversationRepliesContext(ctx context.Context, params *GetConversationRepliesParameters) (msgs []Message, hasMore bool, nextCursor string, err error) {
values := url.Values{
"token": {api.token},
"channel": {params.ChannelID},
"ts": {params.Timestamp},
}
if params.Cursor != "" {
values.Add("cursor", params.Cursor)
}
if params.Latest != "" {
values.Add("latest", params.Latest)
}
if params.Limit != 0 {
values.Add("limit", strconv.Itoa(params.Limit))
}
if params.Oldest != "" {
values.Add("oldest", params.Oldest)
}
if params.Inclusive {
values.Add("inclusive", "1")
} else {
values.Add("inclusive", "0")
}
response := struct {
SlackResponse
HasMore bool `json:"has_more"`
ResponseMetaData struct {
NextCursor string `json:"next_cursor"`
} `json:"response_metadata"`
Messages []Message `json:"messages"`
}{}
err = postSlackMethod(ctx, api.httpclient, "conversations.replies", values, &response, api.debug)
if err != nil {
return nil, false, "", err
}
return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, response.Err()
}
type GetConversationsParameters struct {
Cursor string
ExcludeArchived string
Limit int
Types []string
}
// GetConversations returns the list of channels in a Slack team
func (api *Client) GetConversations(params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) {
return api.GetConversationsContext(context.Background(), params)
}
// GetConversationsContext returns the list of channels in a Slack team with a custom context
func (api *Client) GetConversationsContext(ctx context.Context, params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) {
values := url.Values{
"token": {api.token},
"exclude_archived": {params.ExcludeArchived},
}
if params.Cursor != "" {
values.Add("cursor", params.Cursor)
}
if params.Limit != 0 {
values.Add("limit", strconv.Itoa(params.Limit))
}
if params.Types != nil {
values.Add("types", strings.Join(params.Types, ","))
}
response := struct {
Channels []Channel `json:"channels"`
ResponseMetaData responseMetaData `json:"response_metadata"`
SlackResponse
}{}
err = postSlackMethod(ctx, api.httpclient, "conversations.list", values, &response, api.debug)
if err != nil {
return nil, "", err
}
return response.Channels, response.ResponseMetaData.NextCursor, response.Err()
}
type OpenConversationParameters struct {
ChannelID string
ReturnIM bool
Users []string
}
// OpenConversation opens or resumes a direct message or multi-person direct message
func (api *Client) OpenConversation(params *OpenConversationParameters) (*Channel, bool, bool, error) {
return api.OpenConversationContext(context.Background(), params)
}
// OpenConversationContext opens or resumes a direct message or multi-person direct message with a custom context
func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConversationParameters) (*Channel, bool, bool, error) {
values := url.Values{
"token": {api.token},
"return_im": {strconv.FormatBool(params.ReturnIM)},
}
if params.ChannelID != "" {
values.Add("channel", params.ChannelID)
}
if params.Users != nil {
values.Add("users", strings.Join(params.Users, ","))
}
response := struct {
Channel *Channel `json:"channel"`
NoOp bool `json:"no_op"`
AlreadyOpen bool `json:"already_open"`
SlackResponse
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.open", values, &response, api.debug)
if err != nil {
return nil, false, false, err
}
return response.Channel, response.NoOp, response.AlreadyOpen, response.Err()
}
// JoinConversation joins an existing conversation
func (api *Client) JoinConversation(channelID string) (*Channel, string, []string, error) {
return api.JoinConversationContext(context.Background(), channelID)
}
// JoinConversationContext joins an existing conversation with a custom context
func (api *Client) JoinConversationContext(ctx context.Context, channelID string) (*Channel, string, []string, error) {
values := url.Values{"token": {api.token}, "channel": {channelID}}
response := struct {
Channel *Channel `json:"channel"`
Warning string `json:"warning"`
ResponseMetaData *struct {
Warnings []string `json:"warnings"`
} `json:"response_metadata"`
SlackResponse
}{}
err := postSlackMethod(ctx, api.httpclient, "conversations.join", values, &response, api.debug)
if err != nil {
return nil, "", nil, err
}
if response.Err() != nil {
return nil, "", nil, response.Err()
}
var warnings []string
if response.ResponseMetaData != nil {
warnings = response.ResponseMetaData.Warnings
}
return response.Channel, response.Warning, warnings, nil
}
type GetConversationHistoryParameters struct {
ChannelID string
Cursor string
Inclusive bool
Latest string
Limit int
Oldest string
}
type GetConversationHistoryResponse struct {
SlackResponse
HasMore bool `json:"has_more"`
PinCount int `json:"pin_count"`
Latest string `json:"latest"`
ResponseMetaData struct {
NextCursor string `json:"next_cursor"`
} `json:"response_metadata"`
Messages []Message `json:"messages"`
}
// GetConversationHistory joins an existing conversation
func (api *Client) GetConversationHistory(params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) {
return api.GetConversationHistoryContext(context.Background(), params)
}
// GetConversationHistoryContext joins an existing conversation with a custom context
func (api *Client) GetConversationHistoryContext(ctx context.Context, params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) {
values := url.Values{"token": {api.token}, "channel": {params.ChannelID}}
if params.Cursor != "" {
values.Add("cursor", params.Cursor)
}
if params.Inclusive {
values.Add("inclusive", "1")
} else {
values.Add("inclusive", "0")
}
if params.Latest != "" {
values.Add("latest", params.Latest)
}
if params.Limit != 0 {
values.Add("limit", strconv.Itoa(params.Limit))
}
if params.Oldest != "" {
values.Add("oldest", params.Oldest)
}
response := GetConversationHistoryResponse{}
err := postSlackMethod(ctx, api.httpclient, "conversations.history", values, &response, api.debug)
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return &response, nil
}

107
vendor/github.com/nlopes/slack/dialog.go generated vendored Normal file
View File

@ -0,0 +1,107 @@
package slack
import (
"context"
"encoding/json"
"errors"
)
type DialogTrigger struct {
TriggerId string `json:"trigger_id"` //Required. Must respond within 3 seconds.
Dialog Dialog `json:"dialog"` //Required.
}
type Dialog struct {
CallbackId string `json:"callback_id"` //Required.
Title string `json:"title"` //Required.
SubmitLabel string `json:"submit_label,omitempty"` //Optional. Default value is 'Submit'
NotifyOnCancel bool `json:"notify_on_cancel,omitempty"` //Optional. Default value is false
Elements []DialogElement `json:"elements"` //Required.
}
type DialogElement interface{}
type DialogTextElement struct {
Label string `json:"label"` //Required.
Name string `json:"name"` //Required.
Type string `json:"type"` //Required. Allowed values: "text", "textarea", "select".
Placeholder string `json:"placeholder,omitempty"` //Optional.
Optional bool `json:"optional,omitempty"` //Optional. Default value is false
Value string `json:"value,omitempty"` //Optional.
MaxLength int `json:"max_length,omitempty"` //Optional.
MinLength int `json:"min_length,omitempty"` //Optional,. Default value is 0
Hint string `json:"hint,omitempty"` //Optional.
Subtype string `json:"subtype,omitempty"` //Optional. Allowed values: "email", "number", "tel", "url".
}
type DialogSelectElement struct {
Label string `json:"label"` //Required.
Name string `json:"name"` //Required.
Type string `json:"type"` //Required. Allowed values: "text", "textarea", "select".
Placeholder string `json:"placeholder,omitempty"` //Optional.
Optional bool `json:"optional,omitempty"` //Optional. Default value is false
Value string `json:"value,omitempty"` //Optional.
DataSource string `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external".
SelectedOptions string `json:"selected_options,omitempty"` //Optional. Default value for "external" only
Options []DialogElementOption `json:"options,omitempty"` //One of options or option_groups is required.
OptionGroups []DialogElementOption `json:"option_groups,omitempty"` //Provide up to 100 options.
}
type DialogElementOption struct {
Label string `json:"label"` //Required.
Value string `json:"value"` //Required.
}
// 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"`
}
// 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"`
}
// OpenDialog opens a dialog window where the triggerId originated from
func (api *Client) OpenDialog(triggerId string, dialog Dialog) (err error) {
return api.OpenDialogContext(context.Background(), triggerId, dialog)
}
// OpenDialogContext opens a dialog window where the triggerId originated from with a custom context
func (api *Client) OpenDialogContext(ctx context.Context, triggerId string, dialog Dialog) (err error) {
if triggerId == "" {
return errors.New("received empty parameters")
}
resp := DialogTrigger{
TriggerId: triggerId,
Dialog: dialog,
}
jsonResp, err := json.Marshal(resp)
if err != nil {
return err
}
response := &SlackResponse{}
endpoint := SLACK_API + "dialog.open"
if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonResp, response, api.debug); err != nil {
return err
}
return response.Err()
}

View File

@ -36,9 +36,9 @@ type dndTeamInfoResponse struct {
SlackResponse SlackResponse
} }
func dndRequest(ctx context.Context, path string, values url.Values, debug bool) (*dndResponseFull, error) { func dndRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*dndResponseFull, error) {
response := &dndResponseFull{} response := &dndResponseFull{}
err := post(ctx, path, values, response, debug) err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -56,17 +56,16 @@ func (api *Client) EndDND() error {
// EndDNDContext ends the user's scheduled Do Not Disturb session with a custom context // EndDNDContext ends the user's scheduled Do Not Disturb session with a custom context
func (api *Client) EndDNDContext(ctx context.Context) error { func (api *Client) EndDNDContext(ctx context.Context) error {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
response := &SlackResponse{} response := &SlackResponse{}
if err := post(ctx, "dnd.endDnd", values, response, api.debug); err != nil {
if err := postSlackMethod(ctx, api.httpclient, "dnd.endDnd", values, response, api.debug); err != nil {
return err return err
} }
if !response.Ok {
return errors.New(response.Error) return response.Err()
}
return nil
} }
// EndSnooze ends the current user's snooze mode // EndSnooze ends the current user's snooze mode
@ -77,10 +76,10 @@ func (api *Client) EndSnooze() (*DNDStatus, error) {
// EndSnoozeContext ends the current user's snooze mode with a custom context // EndSnoozeContext ends the current user's snooze mode with a custom context
func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
response, err := dndRequest(ctx, "dnd.endSnooze", values, api.debug) response, err := dndRequest(ctx, api.httpclient, "dnd.endSnooze", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -95,12 +94,13 @@ func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) {
// GetDNDInfoContext provides information about a user's current Do Not Disturb settings with a custom context. // GetDNDInfoContext provides information about a user's current Do Not Disturb settings with a custom context.
func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDStatus, error) { func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDStatus, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
if user != nil { if user != nil {
values.Set("user", *user) values.Set("user", *user)
} }
response, err := dndRequest(ctx, "dnd.info", values, api.debug)
response, err := dndRequest(ctx, api.httpclient, "dnd.info", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -115,11 +115,12 @@ func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error)
// GetDNDTeamInfoContext provides information about a user's current Do Not Disturb settings with a custom context. // GetDNDTeamInfoContext provides information about a user's current Do Not Disturb settings with a custom context.
func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (map[string]DNDStatus, error) { func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (map[string]DNDStatus, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"users": {strings.Join(users, ",")}, "users": {strings.Join(users, ",")},
} }
response := &dndTeamInfoResponse{} response := &dndTeamInfoResponse{}
if err := post(ctx, "dnd.teamInfo", values, response, api.debug); err != nil {
if err := postSlackMethod(ctx, api.httpclient, "dnd.teamInfo", values, response, api.debug); err != nil {
return nil, err return nil, err
} }
if !response.Ok { if !response.Ok {
@ -139,10 +140,11 @@ func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) {
// For more information see the SetSnooze docs // For more information see the SetSnooze docs
func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) { func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"num_minutes": {strconv.Itoa(minutes)}, "num_minutes": {strconv.Itoa(minutes)},
} }
response, err := dndRequest(ctx, "dnd.setSnooze", values, api.debug)
response, err := dndRequest(ctx, api.httpclient, "dnd.setSnooze", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -19,10 +19,11 @@ func (api *Client) GetEmoji() (map[string]string, error) {
// GetEmojiContext retrieves all the emojis with a custom context // GetEmojiContext retrieves all the emojis with a custom context
func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, error) { func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
response := &emojiResponseFull{} response := &emojiResponseFull{}
err := post(ctx, "emoji.list", values, response, api.debug)
err := postSlackMethod(ctx, api.httpclient, "emoji.list", values, response, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -136,9 +136,9 @@ func NewGetFilesParameters() GetFilesParameters {
} }
} }
func fileRequest(ctx context.Context, path string, values url.Values, debug bool) (*fileResponseFull, error) { func fileRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*fileResponseFull, error) {
response := &fileResponseFull{} response := &fileResponseFull{}
err := post(ctx, path, values, response, debug) err := postForm(ctx, client, SLACK_API+path, values, response, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -156,12 +156,13 @@ func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment
// GetFileInfoContext retrieves a file and related comments with a custom context // GetFileInfoContext retrieves a file and related comments with a custom context
func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*File, []Comment, *Paging, error) { func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*File, []Comment, *Paging, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"file": {fileID}, "file": {fileID},
"count": {strconv.Itoa(count)}, "count": {strconv.Itoa(count)},
"page": {strconv.Itoa(page)}, "page": {strconv.Itoa(page)},
} }
response, err := fileRequest(ctx, "files.info", values, api.debug)
response, err := fileRequest(ctx, api.httpclient, "files.info", values, api.debug)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
@ -176,7 +177,7 @@ func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error)
// GetFilesContext retrieves all files according to the parameters given with a custom context // 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) { func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
if params.User != DEFAULT_FILES_USER { if params.User != DEFAULT_FILES_USER {
values.Add("user", params.User) values.Add("user", params.User)
@ -199,7 +200,8 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter
if params.Page != DEFAULT_FILES_PAGE { if params.Page != DEFAULT_FILES_PAGE {
values.Add("page", strconv.Itoa(params.Page)) values.Add("page", strconv.Itoa(params.Page))
} }
response, err := fileRequest(ctx, "files.list", values, api.debug)
response, err := fileRequest(ctx, api.httpclient, "files.list", values, api.debug)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -221,7 +223,7 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam
} }
response := &fileResponseFull{} response := &fileResponseFull{}
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
if params.Filetype != "" { if params.Filetype != "" {
values.Add("filetype", params.Filetype) values.Add("filetype", params.Filetype)
@ -240,11 +242,11 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam
} }
if params.Content != "" { if params.Content != "" {
values.Add("content", params.Content) values.Add("content", params.Content)
err = post(ctx, "files.upload", values, response, api.debug) err = postForm(ctx, api.httpclient, SLACK_API+"files.upload", values, response, api.debug)
} else if params.File != "" { } else if params.File != "" {
err = postLocalWithMultipartResponse(ctx, "files.upload", params.File, "file", values, response, api.debug) err = postLocalWithMultipartResponse(ctx, api.httpclient, "files.upload", params.File, "file", values, response, api.debug)
} else if params.Reader != nil { } else if params.Reader != nil {
err = postWithMultipartResponse(ctx, "files.upload", params.Filename, "file", values, params.Reader, response, api.debug) err = postWithMultipartResponse(ctx, api.httpclient, "files.upload", params.Filename, "file", values, params.Reader, response, api.debug)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -255,20 +257,40 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam
return &response.File, nil return &response.File, nil
} }
// DeleteFileComment deletes a file's comment
func (api *Client) DeleteFileComment(commentID, fileID string) error {
return api.DeleteFileCommentContext(context.Background(), fileID, commentID)
}
// DeleteFileCommentContext deletes a file's comment with a custom context
func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, commentID string) (err error) {
if fileID == "" || commentID == "" {
return errors.New("received empty parameters")
}
values := url.Values{
"token": {api.token},
"file": {fileID},
"id": {commentID},
}
_, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug)
return err
}
// DeleteFile deletes a file // DeleteFile deletes a file
func (api *Client) DeleteFile(fileID string) error { func (api *Client) DeleteFile(fileID string) error {
return api.DeleteFileContext(context.Background(), fileID) return api.DeleteFileContext(context.Background(), fileID)
} }
// DeleteFileContext deletes a file with a custom context // DeleteFileContext deletes a file with a custom context
func (api *Client) DeleteFileContext(ctx context.Context, fileID string) error { func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"file": {fileID}, "file": {fileID},
} }
_, err := fileRequest(ctx, "files.delete", values, api.debug)
return err
_, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug)
return err
} }
// RevokeFilePublicURL disables public/external sharing for a file // RevokeFilePublicURL disables public/external sharing for a file
@ -279,10 +301,11 @@ func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) {
// RevokeFilePublicURLContext disables public/external sharing for a file with a custom context // RevokeFilePublicURLContext disables public/external sharing for a file with a custom context
func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string) (*File, error) { func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string) (*File, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"file": {fileID}, "file": {fileID},
} }
response, err := fileRequest(ctx, "files.revokePublicURL", values, api.debug)
response, err := fileRequest(ctx, api.httpclient, "files.revokePublicURL", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -297,10 +320,11 @@ func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging,
// ShareFilePublicURLContext enabled public/external sharing for a file with a custom context // ShareFilePublicURLContext enabled public/external sharing for a file with a custom context
func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) (*File, []Comment, *Paging, error) { func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) (*File, []Comment, *Paging, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"file": {fileID}, "file": {fileID},
} }
response, err := fileRequest(ctx, "files.sharedPublicURL", values, api.debug)
response, err := fileRequest(ctx, api.httpclient, "files.sharedPublicURL", values, api.debug)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }

View File

@ -28,9 +28,9 @@ type groupResponseFull struct {
SlackResponse SlackResponse
} }
func groupRequest(ctx context.Context, path string, values url.Values, debug bool) (*groupResponseFull, error) { func groupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*groupResponseFull, error) {
response := &groupResponseFull{} response := &groupResponseFull{}
err := post(ctx, path, values, response, debug) err := postForm(ctx, client, SLACK_API+path, values, response, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -45,17 +45,15 @@ func (api *Client) ArchiveGroup(group string) error {
return api.ArchiveGroupContext(context.Background(), group) return api.ArchiveGroupContext(context.Background(), group)
} }
// ArchiveGroup archives a private group // ArchiveGroupContext archives a private group
func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error { func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
} }
_, err := groupRequest(ctx, "groups.archive", values, api.debug)
if err != nil { _, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api.debug)
return err return err
}
return nil
} }
// UnarchiveGroup unarchives a private group // UnarchiveGroup unarchives a private group
@ -63,17 +61,15 @@ func (api *Client) UnarchiveGroup(group string) error {
return api.UnarchiveGroupContext(context.Background(), group) return api.UnarchiveGroupContext(context.Background(), group)
} }
// UnarchiveGroup unarchives a private group // UnarchiveGroupContext unarchives a private group
func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) error { func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) error {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
} }
_, err := groupRequest(ctx, "groups.unarchive", values, api.debug)
if err != nil { _, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api.debug)
return err return err
}
return nil
} }
// CreateGroup creates a private group // CreateGroup creates a private group
@ -81,13 +77,14 @@ func (api *Client) CreateGroup(group string) (*Group, error) {
return api.CreateGroupContext(context.Background(), group) return api.CreateGroupContext(context.Background(), group)
} }
// CreateGroup creates a private group // CreateGroupContext creates a private group
func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group, error) { func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"name": {group}, "name": {group},
} }
response, err := groupRequest(ctx, "groups.create", values, api.debug)
response, err := groupRequest(ctx, api.httpclient, "groups.create", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -104,14 +101,15 @@ func (api *Client) CreateChildGroup(group string) (*Group, error) {
return api.CreateChildGroupContext(context.Background(), group) return api.CreateChildGroupContext(context.Background(), group)
} }
// CreateChildGroup creates a new private group archiving the old one with a custom context // CreateChildGroupContext creates a new private group archiving the old one with a custom context
// For more information see CreateChildGroup // For more information see CreateChildGroup
func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (*Group, error) { func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (*Group, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
} }
response, err := groupRequest(ctx, "groups.createChild", values, api.debug)
response, err := groupRequest(ctx, api.httpclient, "groups.createChild", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -126,10 +124,11 @@ func (api *Client) CloseGroup(group string) (bool, bool, error) {
// CloseGroupContext closes a private group with a custom context // CloseGroupContext closes a private group with a custom context
func (api *Client) CloseGroupContext(ctx context.Context, group string) (bool, bool, error) { func (api *Client) CloseGroupContext(ctx context.Context, group string) (bool, bool, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
} }
response, err := imRequest(ctx, "groups.close", values, api.debug)
response, err := imRequest(ctx, api.httpclient, "groups.close", values, api.debug)
if err != nil { if err != nil {
return false, false, err return false, false, err
} }
@ -144,7 +143,7 @@ func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*His
// GetGroupHistoryContext fetches all the history for a private group with a custom context // GetGroupHistoryContext fetches all the history for a private group with a custom context
func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, params HistoryParameters) (*History, error) { func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, params HistoryParameters) (*History, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
} }
if params.Latest != DEFAULT_HISTORY_LATEST { if params.Latest != DEFAULT_HISTORY_LATEST {
@ -170,7 +169,8 @@ func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, par
values.Add("unreads", "0") values.Add("unreads", "0")
} }
} }
response, err := groupRequest(ctx, "groups.history", values, api.debug)
response, err := groupRequest(ctx, api.httpclient, "groups.history", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -185,11 +185,12 @@ func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) {
// InviteUserToGroupContext invites a specific user to a private group with a custom context // InviteUserToGroupContext invites a specific user to a private group with a custom context
func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user string) (*Group, bool, error) { func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user string) (*Group, bool, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
"user": {user}, "user": {user},
} }
response, err := groupRequest(ctx, "groups.invite", values, api.debug)
response, err := groupRequest(ctx, api.httpclient, "groups.invite", values, api.debug)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
@ -202,12 +203,13 @@ func (api *Client) LeaveGroup(group string) error {
} }
// LeaveGroupContext makes authenticated user leave the group with a custom context // LeaveGroupContext makes authenticated user leave the group with a custom context
func (api *Client) LeaveGroupContext(ctx context.Context, group string) error { func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
} }
_, err := groupRequest(ctx, "groups.leave", values, api.debug)
_, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug)
return err return err
} }
@ -217,13 +219,14 @@ func (api *Client) KickUserFromGroup(group, user string) error {
} }
// KickUserFromGroupContext kicks a user from a group with a custom context // KickUserFromGroupContext kicks a user from a group with a custom context
func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user string) error { func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user string) (err error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
"user": {user}, "user": {user},
} }
_, err := groupRequest(ctx, "groups.kick", values, api.debug)
_, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug)
return err return err
} }
@ -235,12 +238,13 @@ func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) {
// GetGroupsContext retrieves all groups with a custom context // GetGroupsContext retrieves all groups with a custom context
func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) ([]Group, error) { func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) ([]Group, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
if excludeArchived { if excludeArchived {
values.Add("exclude_archived", "1") values.Add("exclude_archived", "1")
} }
response, err := groupRequest(ctx, "groups.list", values, api.debug)
response, err := groupRequest(ctx, api.httpclient, "groups.list", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -255,10 +259,11 @@ func (api *Client) GetGroupInfo(group string) (*Group, error) {
// GetGroupInfoContext retrieves the given group with a custom context // GetGroupInfoContext retrieves the given group with a custom context
func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) { func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
} }
response, err := groupRequest(ctx, "groups.info", values, api.debug)
response, err := groupRequest(ctx, api.httpclient, "groups.info", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -276,13 +281,14 @@ func (api *Client) SetGroupReadMark(group, ts string) error {
// SetGroupReadMarkContext sets the read mark on a private group with a custom context // SetGroupReadMarkContext sets the read mark on a private group with a custom context
// For more details see SetGroupReadMark // For more details see SetGroupReadMark
func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string) error { func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string) (err error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
"ts": {ts}, "ts": {ts},
} }
_, err := groupRequest(ctx, "groups.mark", values, api.debug)
_, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug)
return err return err
} }
@ -294,10 +300,11 @@ func (api *Client) OpenGroup(group string) (bool, bool, error) {
// OpenGroupContext opens a private group with a custom context // OpenGroupContext opens a private group with a custom context
func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bool, error) { func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bool, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
} }
response, err := groupRequest(ctx, "groups.open", values, api.debug)
response, err := groupRequest(ctx, api.httpclient, "groups.open", values, api.debug)
if err != nil { if err != nil {
return false, false, err return false, false, err
} }
@ -314,13 +321,14 @@ func (api *Client) RenameGroup(group, name string) (*Channel, error) {
// RenameGroupContext renames a group with a custom context // RenameGroupContext renames a group with a custom context
func (api *Client) RenameGroupContext(ctx context.Context, group, name string) (*Channel, error) { func (api *Client) RenameGroupContext(ctx context.Context, group, name string) (*Channel, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
"name": {name}, "name": {name},
} }
// XXX: the created entry in this call returns a string instead of a number // 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. // so I may have to do some workaround to solve it.
response, err := groupRequest(ctx, "groups.rename", values, api.debug) response, err := groupRequest(ctx, api.httpclient, "groups.rename", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -335,11 +343,12 @@ func (api *Client) SetGroupPurpose(group, purpose string) (string, error) {
// SetGroupPurposeContext sets the group purpose with a custom context // SetGroupPurposeContext sets the group purpose with a custom context
func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose string) (string, error) { func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose string) (string, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
"purpose": {purpose}, "purpose": {purpose},
} }
response, err := groupRequest(ctx, "groups.setPurpose", values, api.debug)
response, err := groupRequest(ctx, api.httpclient, "groups.setPurpose", values, api.debug)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -354,11 +363,12 @@ func (api *Client) SetGroupTopic(group, topic string) (string, error) {
// SetGroupTopicContext sets the group topic with a custom context // SetGroupTopicContext sets the group topic with a custom context
func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string) (string, error) { func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string) (string, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {group}, "channel": {group},
"topic": {topic}, "topic": {topic},
} }
response, err := groupRequest(ctx, "groups.setTopic", values, api.debug)
response, err := groupRequest(ctx, api.httpclient, "groups.setTopic", values, api.debug)
if err != nil { if err != nil {
return "", err return "", err
} }

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

@ -29,9 +29,9 @@ type IM struct {
IsUserDeleted bool `json:"is_user_deleted"` IsUserDeleted bool `json:"is_user_deleted"`
} }
func imRequest(ctx context.Context, path string, values url.Values, debug bool) (*imResponseFull, error) { func imRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*imResponseFull, error) {
response := &imResponseFull{} response := &imResponseFull{}
err := post(ctx, path, values, response, debug) err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -49,10 +49,11 @@ func (api *Client) CloseIMChannel(channel string) (bool, bool, error) {
// CloseIMChannelContext closes the direct message channel with a custom context // CloseIMChannelContext closes the direct message channel with a custom context
func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (bool, bool, error) { func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (bool, bool, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channel}, "channel": {channel},
} }
response, err := imRequest(ctx, "im.close", values, api.debug)
response, err := imRequest(ctx, api.httpclient, "im.close", values, api.debug)
if err != nil { if err != nil {
return false, false, err return false, false, err
} }
@ -69,10 +70,11 @@ func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) {
// Returns some status and the channel ID // Returns some status and the channel ID
func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool, bool, string, error) { func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool, bool, string, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"user": {user}, "user": {user},
} }
response, err := imRequest(ctx, "im.open", values, api.debug)
response, err := imRequest(ctx, api.httpclient, "im.open", values, api.debug)
if err != nil { if err != nil {
return false, false, "", err return false, false, "", err
} }
@ -85,17 +87,15 @@ func (api *Client) MarkIMChannel(channel, ts string) (err error) {
} }
// MarkIMChannelContext sets the read mark of a direct message channel to a specific point with a custom context // MarkIMChannelContext sets the read mark of a direct message channel to a specific point with a custom context
func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) (err error) { func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) error {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channel}, "channel": {channel},
"ts": {ts}, "ts": {ts},
} }
_, err = imRequest(ctx, "im.mark", values, api.debug)
if err != nil { _, err := imRequest(ctx, api.httpclient, "im.mark", values, api.debug)
return err return err
}
return
} }
// GetIMHistory retrieves the direct message channel history // GetIMHistory retrieves the direct message channel history
@ -106,7 +106,7 @@ func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*Hist
// GetIMHistoryContext retrieves the direct message channel history with a custom context // GetIMHistoryContext retrieves the direct message channel history with a custom context
func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) { func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"channel": {channel}, "channel": {channel},
} }
if params.Latest != DEFAULT_HISTORY_LATEST { if params.Latest != DEFAULT_HISTORY_LATEST {
@ -132,7 +132,8 @@ func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, para
values.Add("unreads", "0") values.Add("unreads", "0")
} }
} }
response, err := imRequest(ctx, "im.history", values, api.debug)
response, err := imRequest(ctx, api.httpclient, "im.history", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -147,9 +148,10 @@ func (api *Client) GetIMChannels() ([]IM, error) {
// GetIMChannelsContext returns the list of direct message channels with a custom context // GetIMChannelsContext returns the list of direct message channels with a custom context
func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) { func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
response, err := imRequest(ctx, "im.list", values, api.debug)
response, err := imRequest(ctx, api.httpclient, "im.list", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,7 +1,9 @@
package slack package slack
import ( import (
"bytes"
"fmt" "fmt"
"strconv"
"time" "time"
) )
@ -127,6 +129,19 @@ func (t JSONTime) Time() time.Time {
return time.Unix(int64(t), 0) return time.Unix(int64(t), 0)
} }
// UnmarshalJSON will unmarshal both string and int JSON values
func (t *JSONTime) UnmarshalJSON(buf []byte) error {
s := bytes.Trim(buf, `"`)
v, err := strconv.Atoi(string(s))
if err != nil {
return err
}
*t = JSONTime(int64(v))
return nil
}
// Team contains details about a team // Team contains details about a team
type Team struct { type Team struct {
ID string `json:"id"` ID string `json:"id"`
@ -156,7 +171,7 @@ type Info struct {
type infoResponseFull struct { type infoResponseFull struct {
Info Info
WebResponse SlackResponse
} }
// GetBotByID returns a bot given a bot id // GetBotByID returns a bot given a bot id

53
vendor/github.com/nlopes/slack/logger.go generated vendored Normal file
View File

@ -0,0 +1,53 @@
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 {
Output(int, string) error
}
// logInternal represents the internal logging api we use.
type logInternal interface {
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
}
// Println replicates the behaviour of the standard logger.
func (t ilogger) 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{}) {
t.Output(2, fmt.Sprintf(format, v...))
}
// Print replicates the behaviour of the standard logger.
func (t ilogger) Print(v ...interface{}) {
t.Output(2, fmt.Sprint(v...))
}

View File

@ -2,12 +2,13 @@ package slack
// OutgoingMessage is used for the realtime API, and seems incomplete. // OutgoingMessage is used for the realtime API, and seems incomplete.
type OutgoingMessage struct { type OutgoingMessage struct {
ID int `json:"id"` ID int `json:"id"`
// channel ID // channel ID
Channel string `json:"channel,omitempty"` Channel string `json:"channel,omitempty"`
Text string `json:"text,omitempty"` Text string `json:"text,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
ThreadTimestamp string `json:"thread_ts,omitempty"` ThreadTimestamp string `json:"thread_ts,omitempty"`
ThreadBroadcast bool `json:"reply_broadcast,omitempty"`
} }
// Message is an auxiliary type to allow us to have a message containing sub messages // Message is an auxiliary type to allow us to have a message containing sub messages
@ -26,9 +27,12 @@ type Msg struct {
Timestamp string `json:"ts,omitempty"` Timestamp string `json:"ts,omitempty"`
ThreadTimestamp string `json:"thread_ts,omitempty"` ThreadTimestamp string `json:"thread_ts,omitempty"`
IsStarred bool `json:"is_starred,omitempty"` IsStarred bool `json:"is_starred,omitempty"`
PinnedTo []string `json:"pinned_to, omitempty"` PinnedTo []string `json:"pinned_to,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"` Attachments []Attachment `json:"attachments,omitempty"`
Edited *Edited `json:"edited,omitempty"` Edited *Edited `json:"edited,omitempty"`
LastRead string `json:"last_read,omitempty"`
Subscribed bool `json:"subscribed,omitempty"`
UnreadCount int `json:"unread_count,omitempty"`
// Message Subtypes // Message Subtypes
SubType string `json:"subtype,omitempty"` SubType string `json:"subtype,omitempty"`
@ -65,7 +69,7 @@ type Msg struct {
ParentUserId string `json:"parent_user_id,omitempty"` ParentUserId string `json:"parent_user_id,omitempty"`
// file_share, file_comment, file_mention // file_share, file_comment, file_mention
File *File `json:"file,omitempty"` Files []File `json:"files,omitempty"`
// file_share // file_share
Upload bool `json:"upload,omitempty"` Upload bool `json:"upload,omitempty"`
@ -82,6 +86,11 @@ type Msg struct {
// reactions // reactions
Reactions []ItemReaction `json:"reactions,omitempty"` Reactions []ItemReaction `json:"reactions,omitempty"`
// slash commands and interactive messages
ResponseType string `json:"response_type,omitempty"`
ReplaceOriginal bool `json:"replace_original"`
DeleteOriginal bool `json:"delete_original"`
} }
// Icon is used for bot messages // Icon is used for bot messages
@ -109,27 +118,33 @@ type Event struct {
// Ping contains information about a Ping Event // Ping contains information about a Ping Event
type Ping struct { type Ping struct {
ID int `json:"id"` ID int `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Timestamp int64 `json:"timestamp"`
} }
// Pong contains information about a Pong Event // Pong contains information about a Pong Event
type Pong struct { type Pong struct {
Type string `json:"type"` Type string `json:"type"`
ReplyTo int `json:"reply_to"` ReplyTo int `json:"reply_to"`
Timestamp int64 `json:"timestamp"`
} }
// NewOutgoingMessage prepares an OutgoingMessage that the user can // NewOutgoingMessage prepares an OutgoingMessage that the user can
// use to send a message. Use this function to properly set the // use to send a message. Use this function to properly set the
// messageID. // messageID.
func (rtm *RTM) NewOutgoingMessage(text string, channelID string) *OutgoingMessage { func (rtm *RTM) NewOutgoingMessage(text string, channelID string, options ...RTMsgOption) *OutgoingMessage {
id := rtm.idGen.Next() id := rtm.idGen.Next()
return &OutgoingMessage{ msg := OutgoingMessage{
ID: id, ID: id,
Type: "message", Type: "message",
Channel: channelID, Channel: channelID,
Text: text, Text: text,
} }
for _, option := range options {
option(&msg)
}
return &msg
} }
// NewTypingMessage prepares an OutgoingMessage that the user can // NewTypingMessage prepares an OutgoingMessage that the user can
@ -143,3 +158,21 @@ func (rtm *RTM) NewTypingMessage(channelID string) *OutgoingMessage {
Channel: channelID, Channel: channelID,
} }
} }
// RTMsgOption allows configuration of various options available for sending an RTM message
type RTMsgOption func(*OutgoingMessage)
// RTMsgOptionTS sets thead timestamp of an outgoing message in order to respond to a thread
func RTMsgOptionTS(threadTimestamp string) RTMsgOption {
return func(msg *OutgoingMessage) {
msg.ThreadTimestamp = threadTimestamp
}
}
// RTMsgOptionBroadcast sets broadcast reply to channel to "true"
func RTMsgOptionBroadcast() RTMsgOption {
return func(msg *OutgoingMessage) {
msg.ThreadBroadcast = true
}
}

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -18,29 +19,41 @@ import (
"time" "time"
) )
// HTTPRequester defines the minimal interface needed for an http.Client to be implemented. type SlackResponse struct {
// Ok bool `json:"ok"`
// Use it in conjunction with the SetHTTPClient function to allow for other capabilities Error string `json:"error"`
// like a tracing http.Client
type HTTPRequester interface {
Do(*http.Request) (*http.Response, error)
} }
var customHTTPClient HTTPRequester func (t SlackResponse) Err() error {
if t.Ok {
return nil
}
// HTTPClient sets a custom http.Client // handle pure text based responses like chat.post
// deprecated: in favor of SetHTTPClient() // which while they have a slack response in their data structure
var HTTPClient = &http.Client{} // it doesn't actually get set during parsing.
if strings.TrimSpace(t.Error) == "" {
return nil
}
type WebResponse struct { return errors.New(t.Error)
Ok bool `json:"ok"`
Error *WebError `json:"error"`
} }
type WebError string // StatusCodeError represents an http response error.
// type httpStatusCode interface { HTTPStatusCode() int } to handle it.
type statusCodeError struct {
Code int
Status string
}
func (s WebError) Error() string { func (t statusCodeError) Error() string {
return string(s) // 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)
}
func (t statusCodeError) HTTPStatusCode() int {
return t.Code
} }
type RateLimitedError struct { type RateLimitedError struct {
@ -77,7 +90,7 @@ func fileUploadReq(ctx context.Context, path, fieldname, filename string, values
return req, nil return req, nil
} }
func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error { func parseResponseBody(body io.ReadCloser, intf interface{}, debug bool) error {
response, err := ioutil.ReadAll(body) response, err := ioutil.ReadAll(body)
if err != nil { if err != nil {
return err return err
@ -88,10 +101,10 @@ func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error
logger.Printf("parseResponseBody: %s\n", string(response)) logger.Printf("parseResponseBody: %s\n", string(response))
} }
return json.Unmarshal(response, &intf) return json.Unmarshal(response, intf)
} }
func postLocalWithMultipartResponse(ctx context.Context, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error { func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error {
fullpath, err := filepath.Abs(fpath) fullpath, err := filepath.Abs(fpath)
if err != nil { if err != nil {
return err return err
@ -101,16 +114,16 @@ func postLocalWithMultipartResponse(ctx context.Context, path, fpath, fieldname
return err return err
} }
defer file.Close() defer file.Close()
return postWithMultipartResponse(ctx, path, filepath.Base(fpath), fieldname, values, file, intf, debug) return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, debug)
} }
func postWithMultipartResponse(ctx context.Context, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, debug bool) error { 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) req, err := fileUploadReq(ctx, SLACK_API+path, fieldname, name, values, r)
if err != nil { if err != nil {
return err return err
} }
req = req.WithContext(ctx) req = req.WithContext(ctx)
resp, err := getHTTPClient().Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return err return err
} }
@ -127,51 +140,68 @@ func postWithMultipartResponse(ctx context.Context, path, name, fieldname string
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it. // Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
logResponse(resp, debug) logResponse(resp, debug)
return fmt.Errorf("Slack server error: %s.", resp.Status) return statusCodeError{Code: resp.StatusCode, Status: resp.Status}
} }
return parseResponseBody(resp.Body, &intf, debug) return parseResponseBody(resp.Body, intf, debug)
} }
func postForm(ctx context.Context, endpoint string, values url.Values, intf interface{}, debug bool) error { func doPost(ctx context.Context, client HTTPRequester, req *http.Request, intf interface{}, debug bool) error {
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// 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)
}
// post JSON.
func postJSON(ctx context.Context, client HTTPRequester, endpoint, token string, json []byte, intf interface{}, debug bool) error {
reqBody := bytes.NewBuffer(json)
req, err := http.NewRequest("POST", endpoint, reqBody)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return doPost(ctx, client, req, intf, debug)
}
// post a url encoded form.
func postForm(ctx context.Context, client HTTPRequester, endpoint string, values url.Values, intf interface{}, debug bool) error {
reqBody := strings.NewReader(values.Encode()) reqBody := strings.NewReader(values.Encode())
req, err := http.NewRequest("POST", endpoint, reqBody) req, err := http.NewRequest("POST", endpoint, reqBody)
if err != nil { if err != nil {
return err return err
} }
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return doPost(ctx, client, req, intf, debug)
req = req.WithContext(ctx)
resp, err := getHTTPClient().Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// 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 fmt.Errorf("Slack server error: %s.", resp.Status)
}
return parseResponseBody(resp.Body, &intf, debug)
} }
func post(ctx context.Context, path string, values url.Values, intf interface{}, debug bool) error { // post to a slack web method.
return postForm(ctx, SLACK_API+path, values, intf, debug) 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 parseAdminResponse(ctx context.Context, method string, teamName string, values url.Values, intf interface{}, debug bool) error { 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()) endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix())
return postForm(ctx, endpoint, values, intf, debug) return postForm(ctx, client, endpoint, values, intf, debug)
} }
func logResponse(resp *http.Response, debug bool) error { func logResponse(resp *http.Response, debug bool) error {
@ -187,17 +217,24 @@ func logResponse(resp *http.Response, debug bool) error {
return nil return nil
} }
func getHTTPClient() HTTPRequester { func okJSONHandler(rw http.ResponseWriter, r *http.Request) {
if customHTTPClient != nil { rw.Header().Set("Content-Type", "application/json")
return customHTTPClient response, _ := json.Marshal(SlackResponse{
Ok: true,
})
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() {
<-t.C
} }
t.Reset(d)
return HTTPClient
}
// 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
} }

View File

@ -55,7 +55,7 @@ func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code,
"redirect_uri": {redirectURI}, "redirect_uri": {redirectURI},
} }
response := &OAuthResponse{} response := &OAuthResponse{}
err = post(ctx, "oauth.access", values, response, debug) err = postSlackMethod(ctx, customHTTPClient, "oauth.access", values, response, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -21,25 +21,24 @@ func (api *Client) AddPin(channel string, item ItemRef) error {
func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemRef) error { func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemRef) error {
values := url.Values{ values := url.Values{
"channel": {channel}, "channel": {channel},
"token": {api.config.token}, "token": {api.token},
} }
if item.Timestamp != "" { if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp)) values.Set("timestamp", item.Timestamp)
} }
if item.File != "" { if item.File != "" {
values.Set("file", string(item.File)) values.Set("file", item.File)
} }
if item.Comment != "" { if item.Comment != "" {
values.Set("file_comment", string(item.Comment)) values.Set("file_comment", item.Comment)
} }
response := &SlackResponse{} response := &SlackResponse{}
if err := post(ctx, "pins.add", values, response, api.debug); err != nil { if err := postSlackMethod(ctx, api.httpclient, "pins.add", values, response, api.debug); err != nil {
return err return err
} }
if !response.Ok {
return errors.New(response.Error) return response.Err()
}
return nil
} }
// RemovePin un-pins an item from a channel // RemovePin un-pins an item from a channel
@ -51,25 +50,24 @@ func (api *Client) RemovePin(channel string, item ItemRef) error {
func (api *Client) RemovePinContext(ctx context.Context, channel string, item ItemRef) error { func (api *Client) RemovePinContext(ctx context.Context, channel string, item ItemRef) error {
values := url.Values{ values := url.Values{
"channel": {channel}, "channel": {channel},
"token": {api.config.token}, "token": {api.token},
} }
if item.Timestamp != "" { if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp)) values.Set("timestamp", item.Timestamp)
} }
if item.File != "" { if item.File != "" {
values.Set("file", string(item.File)) values.Set("file", item.File)
} }
if item.Comment != "" { if item.Comment != "" {
values.Set("file_comment", string(item.Comment)) values.Set("file_comment", item.Comment)
} }
response := &SlackResponse{} response := &SlackResponse{}
if err := post(ctx, "pins.remove", values, response, api.debug); err != nil { if err := postSlackMethod(ctx, api.httpclient, "pins.remove", values, response, api.debug); err != nil {
return err return err
} }
if !response.Ok {
return errors.New(response.Error) return response.Err()
}
return nil
} }
// ListPins returns information about the items a user reacted to. // ListPins returns information about the items a user reacted to.
@ -81,10 +79,11 @@ func (api *Client) ListPins(channel string) ([]Item, *Paging, error) {
func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item, *Paging, error) { func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item, *Paging, error) {
values := url.Values{ values := url.Values{
"channel": {channel}, "channel": {channel},
"token": {api.config.token}, "token": {api.token},
} }
response := &listPinsResponseFull{} response := &listPinsResponseFull{}
err := post(ctx, "pins.list", values, response, api.debug) err := postSlackMethod(ctx, api.httpclient, "pins.list", values, response, api.debug)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -136,31 +136,30 @@ func (api *Client) AddReaction(name string, item ItemRef) error {
// AddReactionContext adds a reaction emoji to a message, file or file comment with a custom context. // AddReactionContext adds a reaction emoji to a message, file or file comment with a custom context.
func (api *Client) AddReactionContext(ctx context.Context, name string, item ItemRef) error { func (api *Client) AddReactionContext(ctx context.Context, name string, item ItemRef) error {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
if name != "" { if name != "" {
values.Set("name", name) values.Set("name", name)
} }
if item.Channel != "" { if item.Channel != "" {
values.Set("channel", string(item.Channel)) values.Set("channel", item.Channel)
} }
if item.Timestamp != "" { if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp)) values.Set("timestamp", item.Timestamp)
} }
if item.File != "" { if item.File != "" {
values.Set("file", string(item.File)) values.Set("file", item.File)
} }
if item.Comment != "" { if item.Comment != "" {
values.Set("file_comment", string(item.Comment)) values.Set("file_comment", item.Comment)
} }
response := &SlackResponse{} response := &SlackResponse{}
if err := post(ctx, "reactions.add", values, response, api.debug); err != nil { if err := postSlackMethod(ctx, api.httpclient, "reactions.add", values, response, api.debug); err != nil {
return err return err
} }
if !response.Ok {
return errors.New(response.Error) return response.Err()
}
return nil
} }
// RemoveReaction removes a reaction emoji from a message, file or file comment. // RemoveReaction removes a reaction emoji from a message, file or file comment.
@ -171,31 +170,30 @@ func (api *Client) RemoveReaction(name string, item ItemRef) error {
// RemoveReactionContext removes a reaction emoji from a message, file or file comment with a custom context. // RemoveReactionContext removes a reaction emoji from a message, file or file comment with a custom context.
func (api *Client) RemoveReactionContext(ctx context.Context, name string, item ItemRef) error { func (api *Client) RemoveReactionContext(ctx context.Context, name string, item ItemRef) error {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
if name != "" { if name != "" {
values.Set("name", name) values.Set("name", name)
} }
if item.Channel != "" { if item.Channel != "" {
values.Set("channel", string(item.Channel)) values.Set("channel", item.Channel)
} }
if item.Timestamp != "" { if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp)) values.Set("timestamp", item.Timestamp)
} }
if item.File != "" { if item.File != "" {
values.Set("file", string(item.File)) values.Set("file", item.File)
} }
if item.Comment != "" { if item.Comment != "" {
values.Set("file_comment", string(item.Comment)) values.Set("file_comment", item.Comment)
} }
response := &SlackResponse{} response := &SlackResponse{}
if err := post(ctx, "reactions.remove", values, response, api.debug); err != nil { if err := postSlackMethod(ctx, api.httpclient, "reactions.remove", values, response, api.debug); err != nil {
return err return err
} }
if !response.Ok {
return errors.New(response.Error) return response.Err()
}
return nil
} }
// GetReactions returns details about the reactions on an item. // GetReactions returns details about the reactions on an item.
@ -206,25 +204,26 @@ func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]
// GetReactionsContext returns details about the reactions on an item with a custom context // GetReactionsContext returns details about the reactions on an item with a custom context
func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
if item.Channel != "" { if item.Channel != "" {
values.Set("channel", string(item.Channel)) values.Set("channel", item.Channel)
} }
if item.Timestamp != "" { if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp)) values.Set("timestamp", item.Timestamp)
} }
if item.File != "" { if item.File != "" {
values.Set("file", string(item.File)) values.Set("file", item.File)
} }
if item.Comment != "" { if item.Comment != "" {
values.Set("file_comment", string(item.Comment)) values.Set("file_comment", item.Comment)
} }
if params.Full != DEFAULT_REACTIONS_FULL { if params.Full != DEFAULT_REACTIONS_FULL {
values.Set("full", strconv.FormatBool(params.Full)) values.Set("full", strconv.FormatBool(params.Full))
} }
response := &getReactionsResponseFull{} response := &getReactionsResponseFull{}
if err := post(ctx, "reactions.get", values, response, api.debug); err != nil { if err := postSlackMethod(ctx, api.httpclient, "reactions.get", values, response, api.debug); err != nil {
return nil, err return nil, err
} }
if !response.Ok { if !response.Ok {
@ -241,7 +240,7 @@ func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem,
// ListReactionsContext returns information about the items a user reacted to with a custom context. // ListReactionsContext returns information about the items a user reacted to with a custom context.
func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, *Paging, error) { func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, *Paging, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
if params.User != DEFAULT_REACTIONS_USER { if params.User != DEFAULT_REACTIONS_USER {
values.Add("user", params.User) values.Add("user", params.User)
@ -255,8 +254,9 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction
if params.Full != DEFAULT_REACTIONS_FULL { if params.Full != DEFAULT_REACTIONS_FULL {
values.Add("full", strconv.FormatBool(params.Full)) values.Add("full", strconv.FormatBool(params.Full))
} }
response := &listReactionsResponseFull{} response := &listReactionsResponseFull{}
err := post(ctx, "reactions.list", values, response, api.debug) err := postSlackMethod(ctx, api.httpclient, "reactions.list", values, response, api.debug)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

110
vendor/github.com/nlopes/slack/rtm.go generated vendored
View File

@ -3,16 +3,34 @@ package slack
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/url" "net/url"
"sync"
"time" "time"
"github.com/gorilla/websocket"
)
const (
websocketDefaultTimeout = 10 * time.Second
defaultPingInterval = 30 * time.Second
)
const (
rtmEventTypeAck = ""
rtmEventTypeHello = "hello"
rtmEventTypeGoodbye = "goodbye"
rtmEventTypePong = "pong"
rtmEventTypeDesktopNotification = "desktop_notification"
) )
// StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info block. // StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info block.
// //
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
func (api *Client) StartRTM() (info *Info, websocketURL string, err error) { func (api *Client) StartRTM() (info *Info, websocketURL string, err error) {
return api.StartRTMContext(context.Background()) ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout)
defer cancel()
return api.StartRTMContext(ctx)
} }
// StartRTMContext calls the "rtm.start" endpoint and returns the provided URL and the full Info block with a custom context. // StartRTMContext calls the "rtm.start" endpoint and returns the provided URL and the full Info block with a custom context.
@ -20,69 +38,101 @@ func (api *Client) StartRTM() (info *Info, websocketURL string, err error) {
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. // 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) { func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
response := &infoResponseFull{} response := &infoResponseFull{}
err = post(ctx, "rtm.start", url.Values{"token": {api.config.token}}, response, api.debug) err = postSlackMethod(ctx, api.httpclient, "rtm.start", url.Values{"token": {api.token}}, response, api.debug)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("post: %s", err) return nil, "", err
}
if !response.Ok {
return nil, "", response.Error
} }
api.Debugln("Using URL:", response.Info.URL) api.Debugln("Using URL:", response.Info.URL)
return &response.Info, response.Info.URL, nil return &response.Info, response.Info.URL, response.Err()
} }
// ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block. // ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block.
// //
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) { func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) {
return api.ConnectRTMContext(context.Background()) ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout)
defer cancel()
return api.ConnectRTMContext(ctx)
} }
// ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block with a custom context. // ConnectRTMContext calls the "rtm.connect" endpoint and returns the
// provided URL and the compact Info block with a custom context.
// //
// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. // 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) { func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
response := &infoResponseFull{} response := &infoResponseFull{}
err = post(ctx, "rtm.connect", url.Values{"token": {api.config.token}}, response, api.debug) err = postSlackMethod(ctx, api.httpclient, "rtm.connect", url.Values{"token": {api.token}}, response, api.debug)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("post: %s", err) api.Debugf("Failed to connect to RTM: %s", err)
} return nil, "", err
if !response.Ok {
return nil, "", response.Error
} }
api.Debugln("Using URL:", response.Info.URL) api.Debugln("Using URL:", response.Info.URL)
return &response.Info, response.Info.URL, nil return &response.Info, response.Info.URL, response.Err()
}
// RTMOption options for the managed RTM.
type RTMOption func(*RTM)
// RTMOptionUseStart 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
func RTMOptionUseStart(b bool) RTMOption {
return func(rtm *RTM) {
rtm.useRTMStart = b
}
}
// RTMOptionDialer takes a gorilla websocket Dialer and uses it as the
// Dialer when opening the websocket for the RTM connection.
func RTMOptionDialer(d *websocket.Dialer) RTMOption {
return func(rtm *RTM) {
rtm.dialer = d
}
}
// RTMOptionPingInterval determines how often to deliver a ping message to slack.
func RTMOptionPingInterval(d time.Duration) RTMOption {
return func(rtm *RTM) {
rtm.pingInterval = d
rtm.resetDeadman()
}
} }
// NewRTM returns a RTM, which provides a fully managed connection to // NewRTM returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol. // Slack's websocket-based Real-Time Messaging protocol.
func (api *Client) NewRTM() *RTM { func (api *Client) NewRTM(options ...RTMOption) *RTM {
return api.NewRTMWithOptions(nil)
}
// NewRTMWithOptions 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 {
result := &RTM{ result := &RTM{
Client: *api, Client: *api,
IncomingEvents: make(chan RTMEvent, 50), IncomingEvents: make(chan RTMEvent, 50),
outgoingMessages: make(chan OutgoingMessage, 20), outgoingMessages: make(chan OutgoingMessage, 20),
pings: make(map[int]time.Time), pingInterval: defaultPingInterval,
pingDeadman: time.NewTimer(deadmanDuration(defaultPingInterval)),
isConnected: false, isConnected: false,
wasIntentional: true, wasIntentional: true,
killChannel: make(chan bool), killChannel: make(chan bool),
disconnected: make(chan struct{}), disconnected: make(chan struct{}, 1),
forcePing: make(chan bool), forcePing: make(chan bool),
rawEvents: make(chan json.RawMessage), rawEvents: make(chan json.RawMessage),
idGen: NewSafeID(1), idGen: NewSafeID(1),
mu: &sync.Mutex{},
} }
if options != nil { for _, opt := range options {
result.useRTMStart = options.UseRTMStart opt(result)
} else {
result.useRTMStart = true
} }
return result 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

@ -11,7 +11,7 @@ const (
DEFAULT_SEARCH_SORT = "score" DEFAULT_SEARCH_SORT = "score"
DEFAULT_SEARCH_SORT_DIR = "desc" DEFAULT_SEARCH_SORT_DIR = "desc"
DEFAULT_SEARCH_HIGHLIGHT = false DEFAULT_SEARCH_HIGHLIGHT = false
DEFAULT_SEARCH_COUNT = 100 DEFAULT_SEARCH_COUNT = 20
DEFAULT_SEARCH_PAGE = 1 DEFAULT_SEARCH_PAGE = 1
) )
@ -37,17 +37,18 @@ type CtxMessage struct {
} }
type SearchMessage struct { type SearchMessage struct {
Type string `json:"type"` Type string `json:"type"`
Channel CtxChannel `json:"channel"` Channel CtxChannel `json:"channel"`
User string `json:"user"` User string `json:"user"`
Username string `json:"username"` Username string `json:"username"`
Timestamp string `json:"ts"` Timestamp string `json:"ts"`
Text string `json:"text"` Text string `json:"text"`
Permalink string `json:"permalink"` Permalink string `json:"permalink"`
Previous CtxMessage `json:"previous"` Attachments []Attachment `json:"attachments"`
Previous2 CtxMessage `json:"previous_2"` Previous CtxMessage `json:"previous"`
Next CtxMessage `json:"next"` Previous2 CtxMessage `json:"previous_2"`
Next2 CtxMessage `json:"next_2"` Next CtxMessage `json:"next"`
Next2 CtxMessage `json:"next_2"`
} }
type SearchMessages struct { type SearchMessages struct {
@ -83,7 +84,7 @@ func NewSearchParameters() SearchParameters {
func (api *Client) _search(ctx context.Context, path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) { func (api *Client) _search(ctx context.Context, path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"query": {query}, "query": {query},
} }
if params.Sort != DEFAULT_SEARCH_SORT { if params.Sort != DEFAULT_SEARCH_SORT {
@ -101,8 +102,9 @@ func (api *Client) _search(ctx context.Context, path, query string, params Searc
if params.Page != DEFAULT_SEARCH_PAGE { if params.Page != DEFAULT_SEARCH_PAGE {
values.Add("page", strconv.Itoa(params.Page)) values.Add("page", strconv.Itoa(params.Page))
} }
response = &searchResponseFull{} response = &searchResponseFull{}
err := post(ctx, path, values, response, api.debug) err := postSlackMethod(ctx, api.httpclient, path, values, response, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -5,20 +5,47 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"net/http"
"net/url" "net/url"
"os" "os"
) )
var logger stdLogger // A logger that can be set by consumers // Added as a var so that we can change this for testing purposes
/*
Added as a var so that we can change this for testing purposes
*/
var SLACK_API string = "https://slack.com/api/" var SLACK_API string = "https://slack.com/api/"
var SLACK_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s" var SLACK_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s"
type SlackResponse struct { // HTTPClient sets a custom http.Client
Ok bool `json:"ok"` // deprecated: in favor of SetHTTPClient()
Error string `json:"error"` 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 {
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"`
}
func (t *ResponseMetadata) initialize() *ResponseMetadata {
if t != nil {
return t
}
return &ResponseMetadata{}
} }
type AuthTestResponse struct { type AuthTestResponse struct {
@ -35,41 +62,33 @@ type authTestResponseFull struct {
} }
type Client struct { type Client struct {
config struct { token string
token string info Info
debug bool
httpclient HTTPRequester
}
// 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
} }
info Info
debug bool
} }
// stdLogger is a logger interface compatible with both stdlib and some // New builds a slack client from the provided token and options.
// 3rd party loggers such as logrus. func New(token string, options ...Option) *Client {
type stdLogger interface { s := &Client{
Print(...interface{}) token: token,
Printf(string, ...interface{}) httpclient: customHTTPClient,
Println(...interface{}) }
Fatal(...interface{}) for _, opt := range options {
Fatalf(string, ...interface{}) opt(s)
Fatalln(...interface{}) }
Panic(...interface{})
Panicf(string, ...interface{})
Panicln(...interface{})
Output(int, string) error
}
// 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 stdLogger) {
logger = l
}
// New creates new Client.
func New(token string) *Client {
s := &Client{}
s.config.token = token
return s return s
} }
@ -80,14 +99,19 @@ 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 // 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, error error) {
api.Debugf("Challenging auth...")
responseFull := &authTestResponseFull{} responseFull := &authTestResponseFull{}
err := post(ctx, "auth.test", url.Values{"token": {api.config.token}}, responseFull, api.debug) err := postSlackMethod(ctx, api.httpclient, "auth.test", url.Values{"token": {api.token}}, responseFull, api.debug)
if err != nil { if err != nil {
api.Debugf("failed to test for auth: %s", err)
return nil, err return nil, err
} }
if !responseFull.Ok { if !responseFull.Ok {
api.Debugf("auth response was not Ok: %s", responseFull.Error)
return nil, errors.New(responseFull.Error) return nil, errors.New(responseFull.Error)
} }
api.Debugf("Auth challenge was successful with response %+v", responseFull.AuthTestResponse)
return &responseFull.AuthTestResponse, nil return &responseFull.AuthTestResponse, nil
} }
@ -97,16 +121,18 @@ func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestRespo
func (api *Client) SetDebug(debug bool) { func (api *Client) SetDebug(debug bool) {
api.debug = debug api.debug = debug
if debug && logger == nil { if debug && logger == nil {
logger = log.New(os.Stdout, "nlopes/slack", log.LstdFlags|log.Lshortfile) SetLogger(log.New(os.Stdout, "nlopes/slack", log.LstdFlags|log.Lshortfile))
} }
} }
// Debugf print a formatted debug line.
func (api *Client) Debugf(format string, v ...interface{}) { func (api *Client) Debugf(format string, v ...interface{}) {
if api.debug { if api.debug {
logger.Output(2, fmt.Sprintf(format, v...)) logger.Output(2, fmt.Sprintf(format, v...))
} }
} }
// Debugln print a debug line.
func (api *Client) Debugln(v ...interface{}) { func (api *Client) Debugln(v ...interface{}) {
if api.debug { if api.debug {
logger.Output(2, fmt.Sprintln(v...)) logger.Output(2, fmt.Sprintln(v...))

53
vendor/github.com/nlopes/slack/slash.go generated vendored Normal file
View File

@ -0,0 +1,53 @@
package slack
import (
"net/http"
)
// SlashCommand contains information about a request of the slash command
type SlashCommand struct {
Token string `json:"token"`
TeamID string `json:"team_id"`
TeamDomain string `json:"team_domain"`
EnterpriseID string `json:"enterprise_id,omitempty"`
EnterpriseName string `json:"enterprise_name,omitempty"`
ChannelID string `json:"channel_id"`
ChannelName string `json:"channel_name"`
UserID string `json:"user_id"`
UserName string `json:"user_name"`
Command string `json:"command"`
Text string `json:"text"`
ResponseURL string `json:"response_url"`
TriggerID string `json:"trigger_id"`
}
// SlashCommandParse will parse the request of the slash command
func SlashCommandParse(r *http.Request) (s SlashCommand, err error) {
if err = r.ParseForm(); err != nil {
return s, err
}
s.Token = r.PostForm.Get("token")
s.TeamID = r.PostForm.Get("team_id")
s.TeamDomain = r.PostForm.Get("team_domain")
s.EnterpriseID = r.PostForm.Get("enterprise_id")
s.EnterpriseName = r.PostForm.Get("enterprise_name")
s.ChannelID = r.PostForm.Get("channel_id")
s.ChannelName = r.PostForm.Get("channel_name")
s.UserID = r.PostForm.Get("user_id")
s.UserName = r.PostForm.Get("user_name")
s.Command = r.PostForm.Get("command")
s.Text = r.PostForm.Get("text")
s.ResponseURL = r.PostForm.Get("response_url")
s.TriggerID = r.PostForm.Get("trigger_id")
return s, nil
}
// ValidateToken validates verificationTokens
func (s SlashCommand) ValidateToken(verificationTokens ...string) bool {
for _, token := range verificationTokens {
if s.Token == token {
return true
}
}
return false
}

View File

@ -45,25 +45,24 @@ func (api *Client) AddStar(channel string, item ItemRef) error {
func (api *Client) AddStarContext(ctx context.Context, channel string, item ItemRef) error { func (api *Client) AddStarContext(ctx context.Context, channel string, item ItemRef) error {
values := url.Values{ values := url.Values{
"channel": {channel}, "channel": {channel},
"token": {api.config.token}, "token": {api.token},
} }
if item.Timestamp != "" { if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp)) values.Set("timestamp", item.Timestamp)
} }
if item.File != "" { if item.File != "" {
values.Set("file", string(item.File)) values.Set("file", item.File)
} }
if item.Comment != "" { if item.Comment != "" {
values.Set("file_comment", string(item.Comment)) values.Set("file_comment", item.Comment)
} }
response := &SlackResponse{} response := &SlackResponse{}
if err := post(ctx, "stars.add", values, response, api.debug); err != nil { if err := postSlackMethod(ctx, api.httpclient, "stars.add", values, response, api.debug); err != nil {
return err return err
} }
if !response.Ok {
return errors.New(response.Error) return response.Err()
}
return nil
} }
// RemoveStar removes a starred item from a channel // RemoveStar removes a starred item from a channel
@ -75,25 +74,24 @@ func (api *Client) RemoveStar(channel string, item ItemRef) error {
func (api *Client) RemoveStarContext(ctx context.Context, channel string, item ItemRef) error { func (api *Client) RemoveStarContext(ctx context.Context, channel string, item ItemRef) error {
values := url.Values{ values := url.Values{
"channel": {channel}, "channel": {channel},
"token": {api.config.token}, "token": {api.token},
} }
if item.Timestamp != "" { if item.Timestamp != "" {
values.Set("timestamp", string(item.Timestamp)) values.Set("timestamp", item.Timestamp)
} }
if item.File != "" { if item.File != "" {
values.Set("file", string(item.File)) values.Set("file", item.File)
} }
if item.Comment != "" { if item.Comment != "" {
values.Set("file_comment", string(item.Comment)) values.Set("file_comment", item.Comment)
} }
response := &SlackResponse{} response := &SlackResponse{}
if err := post(ctx, "stars.remove", values, response, api.debug); err != nil { if err := postSlackMethod(ctx, api.httpclient, "stars.remove", values, response, api.debug); err != nil {
return err return err
} }
if !response.Ok {
return errors.New(response.Error) return response.Err()
}
return nil
} }
// ListStars returns information about the stars a user added // ListStars returns information about the stars a user added
@ -104,7 +102,7 @@ func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) {
// ListStarsContext returns information about the stars a user added with a custom context // ListStarsContext returns information about the stars a user added with a custom context
func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, *Paging, error) { func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, *Paging, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
if params.User != DEFAULT_STARS_USER { if params.User != DEFAULT_STARS_USER {
values.Add("user", params.User) values.Add("user", params.User)
@ -115,8 +113,9 @@ func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters)
if params.Page != DEFAULT_STARS_PAGE { if params.Page != DEFAULT_STARS_PAGE {
values.Add("page", strconv.Itoa(params.Page)) values.Add("page", strconv.Itoa(params.Page))
} }
response := &listResponseFull{} response := &listResponseFull{}
err := post(ctx, "stars.list", values, response, api.debug) err := postSlackMethod(ctx, api.httpclient, "stars.list", values, response, api.debug)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -67,9 +67,9 @@ func NewAccessLogParameters() AccessLogParameters {
} }
} }
func teamRequest(ctx context.Context, path string, values url.Values, debug bool) (*TeamResponse, error) { func teamRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*TeamResponse, error) {
response := &TeamResponse{} response := &TeamResponse{}
err := post(ctx, path, values, response, debug) err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -81,9 +81,9 @@ func teamRequest(ctx context.Context, path string, values url.Values, debug bool
return response, nil return response, nil
} }
func billableInfoRequest(ctx context.Context, path string, values url.Values, debug bool) (map[string]BillingActive, error) { func billableInfoRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (map[string]BillingActive, error) {
response := &BillableInfoResponse{} response := &BillableInfoResponse{}
err := post(ctx, path, values, response, debug) err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -95,9 +95,9 @@ func billableInfoRequest(ctx context.Context, path string, values url.Values, de
return response.BillableInfo, nil return response.BillableInfo, nil
} }
func accessLogsRequest(ctx context.Context, path string, values url.Values, debug bool) (*LoginResponse, error) { func accessLogsRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*LoginResponse, error) {
response := &LoginResponse{} response := &LoginResponse{}
err := post(ctx, path, values, response, debug) err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -115,10 +115,10 @@ func (api *Client) GetTeamInfo() (*TeamInfo, error) {
// GetTeamInfoContext gets the Team Information of the user with a custom context // GetTeamInfoContext gets the Team Information of the user with a custom context
func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) { func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
response, err := teamRequest(ctx, "team.info", values, api.debug) response, err := teamRequest(ctx, api.httpclient, "team.info", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -133,7 +133,7 @@ func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging,
// GetAccessLogsContext retrieves a page of logins according to the parameters given with a custom context // GetAccessLogsContext retrieves a page of logins according to the parameters given with a custom context
func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, *Paging, error) { func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, *Paging, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
if params.Count != DEFAULT_LOGINS_COUNT { if params.Count != DEFAULT_LOGINS_COUNT {
values.Add("count", strconv.Itoa(params.Count)) values.Add("count", strconv.Itoa(params.Count))
@ -141,7 +141,8 @@ func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogPar
if params.Page != DEFAULT_LOGINS_PAGE { if params.Page != DEFAULT_LOGINS_PAGE {
values.Add("page", strconv.Itoa(params.Page)) values.Add("page", strconv.Itoa(params.Page))
} }
response, err := accessLogsRequest(ctx, "team.accessLogs", values, api.debug)
response, err := accessLogsRequest(ctx, api.httpclient, "team.accessLogs", values, api.debug)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -154,11 +155,11 @@ func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error
func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map[string]BillingActive, error) { func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map[string]BillingActive, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"user": {user}, "user": {user},
} }
return billableInfoRequest(ctx, "team.billableInfo", values, api.debug) return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug)
} }
// GetBillableInfoForTeam returns the billing_active status of all users on the team. // GetBillableInfoForTeam returns the billing_active status of all users on the team.
@ -169,8 +170,8 @@ func (api *Client) GetBillableInfoForTeam() (map[string]BillingActive, error) {
// GetBillableInfoForTeamContext returns the billing_active status of all users on the team with a custom context // GetBillableInfoForTeamContext returns the billing_active status of all users on the team with a custom context
func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[string]BillingActive, error) { func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[string]BillingActive, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
return billableInfoRequest(ctx, "team.billableInfo", values, api.debug) return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug)
} }

View File

@ -40,9 +40,9 @@ type userGroupResponseFull struct {
SlackResponse SlackResponse
} }
func userGroupRequest(ctx context.Context, path string, values url.Values, debug bool) (*userGroupResponseFull, error) { func userGroupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userGroupResponseFull, error) {
response := &userGroupResponseFull{} response := &userGroupResponseFull{}
err := post(ctx, path, values, response, debug) err := postSlackMethod(ctx, client, path, values, response, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -60,7 +60,7 @@ func (api *Client) CreateUserGroup(userGroup UserGroup) (UserGroup, error) {
// CreateUserGroupContext creates a new user group with a custom context // CreateUserGroupContext creates a new user group with a custom context
func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) { func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"name": {userGroup.Name}, "name": {userGroup.Name},
} }
@ -76,7 +76,7 @@ func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGro
values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")} values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")}
} }
response, err := userGroupRequest(ctx, "usergroups.create", values, api.debug) response, err := userGroupRequest(ctx, api.httpclient, "usergroups.create", values, api.debug)
if err != nil { if err != nil {
return UserGroup{}, err return UserGroup{}, err
} }
@ -91,11 +91,11 @@ func (api *Client) DisableUserGroup(userGroup string) (UserGroup, error) {
// DisableUserGroupContext disables an existing user group with a custom context // DisableUserGroupContext disables an existing user group with a custom context
func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) { func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"usergroup": {userGroup}, "usergroup": {userGroup},
} }
response, err := userGroupRequest(ctx, "usergroups.disable", values, api.debug) response, err := userGroupRequest(ctx, api.httpclient, "usergroups.disable", values, api.debug)
if err != nil { if err != nil {
return UserGroup{}, err return UserGroup{}, err
} }
@ -110,11 +110,11 @@ func (api *Client) EnableUserGroup(userGroup string) (UserGroup, error) {
// EnableUserGroupContext enables an existing user group with a custom context // EnableUserGroupContext enables an existing user group with a custom context
func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) { func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"usergroup": {userGroup}, "usergroup": {userGroup},
} }
response, err := userGroupRequest(ctx, "usergroups.enable", values, api.debug) response, err := userGroupRequest(ctx, api.httpclient, "usergroups.enable", values, api.debug)
if err != nil { if err != nil {
return UserGroup{}, err return UserGroup{}, err
} }
@ -129,10 +129,10 @@ func (api *Client) GetUserGroups() ([]UserGroup, error) {
// GetUserGroupsContext returns a list of user groups for the team with a custom context // GetUserGroupsContext returns a list of user groups for the team with a custom context
func (api *Client) GetUserGroupsContext(ctx context.Context) ([]UserGroup, error) { func (api *Client) GetUserGroupsContext(ctx context.Context) ([]UserGroup, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
response, err := userGroupRequest(ctx, "usergroups.list", values, api.debug) response, err := userGroupRequest(ctx, api.httpclient, "usergroups.list", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -147,7 +147,7 @@ func (api *Client) UpdateUserGroup(userGroup UserGroup) (UserGroup, error) {
// UpdateUserGroupContext will update an existing user group with a custom context // UpdateUserGroupContext will update an existing user group with a custom context
func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) { func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"usergroup": {userGroup.ID}, "usergroup": {userGroup.ID},
} }
@ -163,7 +163,7 @@ func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGro
values["description"] = []string{userGroup.Description} values["description"] = []string{userGroup.Description}
} }
response, err := userGroupRequest(ctx, "usergroups.update", values, api.debug) response, err := userGroupRequest(ctx, api.httpclient, "usergroups.update", values, api.debug)
if err != nil { if err != nil {
return UserGroup{}, err return UserGroup{}, err
} }
@ -178,11 +178,11 @@ func (api *Client) GetUserGroupMembers(userGroup string) ([]string, error) {
// GetUserGroupMembersContext will retrieve the current list of users in a group with a custom context // GetUserGroupMembersContext will retrieve the current list of users in a group with a custom context
func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup string) ([]string, error) { func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup string) ([]string, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"usergroup": {userGroup}, "usergroup": {userGroup},
} }
response, err := userGroupRequest(ctx, "usergroups.users.list", values, api.debug) response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.list", values, api.debug)
if err != nil { if err != nil {
return []string{}, err return []string{}, err
} }
@ -197,12 +197,12 @@ func (api *Client) UpdateUserGroupMembers(userGroup string, members string) (Use
// UpdateUserGroupMembersContext will update the members of an existing user group with a custom context // UpdateUserGroupMembersContext will update the members of an existing user group with a custom context
func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup string, members string) (UserGroup, error) { func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup string, members string) (UserGroup, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"usergroup": {userGroup}, "usergroup": {userGroup},
"users": {members}, "users": {members},
} }
response, err := userGroupRequest(ctx, "usergroups.users.update", values, api.debug) response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.update", values, api.debug)
if err != nil { if err != nil {
return UserGroup{}, err return UserGroup{}, err
} }

View File

@ -5,41 +5,103 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"net/url" "net/url"
"strconv"
) )
const ( const (
DEFAULT_USER_PHOTO_CROP_X = -1 DEFAULT_USER_PHOTO_CROP_X = -1
DEFAULT_USER_PHOTO_CROP_Y = -1 DEFAULT_USER_PHOTO_CROP_Y = -1
DEFAULT_USER_PHOTO_CROP_W = -1 DEFAULT_USER_PHOTO_CROP_W = -1
errPaginationComplete = errorString("pagination complete")
) )
// UserProfile contains all the information details of a given user // UserProfile contains all the information details of a given user
type UserProfile struct { type UserProfile struct {
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
RealName string `json:"real_name"` RealName string `json:"real_name"`
RealNameNormalized string `json:"real_name_normalized"` RealNameNormalized string `json:"real_name_normalized"`
DisplayName string `json:"display_name"` DisplayName string `json:"display_name"`
DisplayNameNormalized string `json:"display_name_normalized"` DisplayNameNormalized string `json:"display_name_normalized"`
Email string `json:"email"` Email string `json:"email"`
Skype string `json:"skype"` Skype string `json:"skype"`
Phone string `json:"phone"` Phone string `json:"phone"`
Image24 string `json:"image_24"` Image24 string `json:"image_24"`
Image32 string `json:"image_32"` Image32 string `json:"image_32"`
Image48 string `json:"image_48"` Image48 string `json:"image_48"`
Image72 string `json:"image_72"` Image72 string `json:"image_72"`
Image192 string `json:"image_192"` Image192 string `json:"image_192"`
ImageOriginal string `json:"image_original"` ImageOriginal string `json:"image_original"`
Title string `json:"title"` Title string `json:"title"`
BotID string `json:"bot_id,omitempty"` BotID string `json:"bot_id,omitempty"`
ApiAppID string `json:"api_app_id,omitempty"` ApiAppID string `json:"api_app_id,omitempty"`
StatusText string `json:"status_text,omitempty"` StatusText string `json:"status_text,omitempty"`
StatusEmoji string `json:"status_emoji,omitempty"` StatusEmoji string `json:"status_emoji,omitempty"`
Team string `json:"team"`
Fields UserProfileCustomFields `json:"fields"`
}
// UserProfileCustomFields represents user profile's custom fields.
// Slack API's response data type is inconsistent so we use the struct.
// For detail, please see below.
// https://github.com/nlopes/slack/pull/298#discussion_r185159233
type UserProfileCustomFields struct {
fields map[string]UserProfileCustomField
}
// UnmarshalJSON is the implementation of the json.Unmarshaler interface.
func (fields *UserProfileCustomFields) UnmarshalJSON(b []byte) error {
// https://github.com/nlopes/slack/pull/298#discussion_r185159233
if string(b) == "[]" {
return nil
}
return json.Unmarshal(b, &fields.fields)
}
// MarshalJSON is the implementation of the json.Marshaler interface.
func (fields UserProfileCustomFields) MarshalJSON() ([]byte, error) {
if len(fields.fields) == 0 {
return []byte("[]"), nil
}
return json.Marshal(fields.fields)
}
// ToMap returns a map of custom fields.
func (fields *UserProfileCustomFields) ToMap() map[string]UserProfileCustomField {
return fields.fields
}
// Len returns the number of custom fields.
func (fields *UserProfileCustomFields) Len() int {
return len(fields.fields)
}
// SetMap sets a map of custom fields.
func (fields *UserProfileCustomFields) SetMap(m map[string]UserProfileCustomField) {
fields.fields = m
}
// FieldsMap returns a map of custom fields.
func (profile *UserProfile) FieldsMap() map[string]UserProfileCustomField {
return profile.Fields.ToMap()
}
// SetFieldsMap sets a map of custom fields.
func (profile *UserProfile) SetFieldsMap(m map[string]UserProfileCustomField) {
profile.Fields.SetMap(m)
}
// UserProfileCustomField represents a custom user profile field
type UserProfileCustomField struct {
Value string `json:"value"`
Alt string `json:"alt"`
Label string `json:"label"`
} }
// User contains all the information of a user // User contains all the information of a user
type User struct { type User struct {
ID string `json:"id"` ID string `json:"id"`
TeamID string `json:"team_id"`
Name string `json:"name"` Name string `json:"name"`
Deleted bool `json:"deleted"` Deleted bool `json:"deleted"`
Color string `json:"color"` Color string `json:"color"`
@ -54,9 +116,12 @@ type User struct {
IsPrimaryOwner bool `json:"is_primary_owner"` IsPrimaryOwner bool `json:"is_primary_owner"`
IsRestricted bool `json:"is_restricted"` IsRestricted bool `json:"is_restricted"`
IsUltraRestricted bool `json:"is_ultra_restricted"` IsUltraRestricted bool `json:"is_ultra_restricted"`
IsStranger bool `json:"is_stranger"`
IsAppUser bool `json:"is_app_user"`
Has2FA bool `json:"has_2fa"` Has2FA bool `json:"has_2fa"`
HasFiles bool `json:"has_files"` HasFiles bool `json:"has_files"`
Presence string `json:"presence"` Presence string `json:"presence"`
Locale string `json:"locale"`
} }
// UserPresence contains details about a user online status // UserPresence contains details about a user online status
@ -103,10 +168,11 @@ type TeamIdentity struct {
} }
type userResponseFull struct { type userResponseFull struct {
Members []User `json:"members,omitempty"` // ListUsers Members []User `json:"members,omitempty"`
User `json:"user,omitempty"` // GetUserInfo User `json:"user,omitempty"`
UserPresence // GetUserPresence UserPresence
SlackResponse SlackResponse
Metadata ResponseMetadata `json:"response_metadata"`
} }
type UserSetPhotoParams struct { type UserSetPhotoParams struct {
@ -123,9 +189,9 @@ func NewUserSetPhotoParams() UserSetPhotoParams {
} }
} }
func userRequest(ctx context.Context, path string, values url.Values, debug bool) (*userResponseFull, error) { func userRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userResponseFull, error) {
response := &userResponseFull{} response := &userResponseFull{}
err := post(ctx, path, values, response, debug) err := postForm(ctx, client, SLACK_API+path, values, response, debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -143,10 +209,11 @@ func (api *Client) GetUserPresence(user string) (*UserPresence, error) {
// GetUserPresenceContext will retrieve the current presence status of given user with a custom context. // GetUserPresenceContext will retrieve the current presence status of given user with a custom context.
func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*UserPresence, error) { func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*UserPresence, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"user": {user}, "user": {user},
} }
response, err := userRequest(ctx, "users.getPresence", values, api.debug)
response, err := userRequest(ctx, api.httpclient, "users.getPresence", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -161,32 +228,138 @@ func (api *Client) GetUserInfo(user string) (*User, error) {
// GetUserInfoContext will retrieve the complete user information with a custom context // GetUserInfoContext will retrieve the complete user information with a custom context
func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) { func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"user": {user}, "user": {user},
} }
response, err := userRequest(ctx, "users.info", values, api.debug)
response, err := userRequest(ctx, api.httpclient, "users.info", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &response.User, nil return &response.User, nil
} }
// GetUsersOption options for the GetUsers method call.
type GetUsersOption func(*UserPagination)
// GetUsersOptionLimit limit the number of users returned
func GetUsersOptionLimit(n int) GetUsersOption {
return func(p *UserPagination) {
p.limit = n
}
}
// GetUsersOptionPresence include user presence
func GetUsersOptionPresence(n bool) GetUsersOption {
return func(p *UserPagination) {
p.presence = n
}
}
func newUserPagination(c *Client, options ...GetUsersOption) (up UserPagination) {
up = UserPagination{
c: c,
limit: 200, // per slack api documentation.
}
for _, opt := range options {
opt(&up)
}
return up
}
// UserPagination allows for paginating over the users
type UserPagination struct {
Users []User
limit int
presence bool
previousResp *ResponseMetadata
c *Client
}
// Done checks if the pagination has completed
func (UserPagination) Done(err error) bool {
return err == errPaginationComplete
}
// Failure checks if pagination failed.
func (t UserPagination) Failure(err error) error {
if t.Done(err) {
return nil
}
return err
}
func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) {
var (
resp *userResponseFull
)
if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") {
return t, errPaginationComplete
}
t.previousResp = t.previousResp.initialize()
values := url.Values{
"limit": {strconv.Itoa(t.limit)},
"presence": {strconv.FormatBool(t.presence)},
"token": {t.c.token},
"cursor": {t.previousResp.Cursor},
}
if resp, err = userRequest(ctx, t.c.httpclient, "users.list", values, t.c.debug); err != nil {
return t, err
}
t.c.Debugf("GetUsersContext: got %d users; metadata %v", len(resp.Members), resp.Metadata)
t.Users = resp.Members
t.previousResp = &resp.Metadata
return t, nil
}
// GetUsersPaginated fetches users in a paginated fashion, see GetUsersContext for usage.
func (api *Client) GetUsersPaginated(options ...GetUsersOption) UserPagination {
return newUserPagination(api, options...)
}
// GetUsers returns the list of users (with their detailed information) // GetUsers returns the list of users (with their detailed information)
func (api *Client) GetUsers() ([]User, error) { func (api *Client) GetUsers() ([]User, error) {
return api.GetUsersContext(context.Background()) return api.GetUsersContext(context.Background())
} }
// GetUsersContext returns the list of users (with their detailed information) with a custom context // GetUsersContext returns the list of users (with their detailed information) with a custom context
func (api *Client) GetUsersContext(ctx context.Context) ([]User, error) { func (api *Client) GetUsersContext(ctx context.Context) (results []User, err error) {
values := url.Values{ var (
"token": {api.config.token}, p UserPagination
"presence": {"1"}, )
for p = api.GetUsersPaginated(); !p.Done(err); p, err = p.Next(ctx) {
results = append(results, p.Users...)
} }
response, err := userRequest(ctx, "users.list", values, api.debug)
return results, p.Failure(err)
}
// GetUserByEmail will retrieve the complete user information by email
func (api *Client) GetUserByEmail(email string) (*User, error) {
return api.GetUserByEmailContext(context.Background(), email)
}
// GetUserByEmailContext will retrieve the complete user information by email with a custom context
func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*User, error) {
values := url.Values{
"token": {api.token},
"email": {email},
}
response, err := userRequest(ctx, api.httpclient, "users.lookupByEmail", values, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return response.Members, nil return &response.User, nil
} }
// SetUserAsActive marks the currently authenticated user as active // SetUserAsActive marks the currently authenticated user as active
@ -195,11 +368,12 @@ func (api *Client) SetUserAsActive() error {
} }
// SetUserAsActiveContext marks the currently authenticated user as active with a custom context // SetUserAsActiveContext marks the currently authenticated user as active with a custom context
func (api *Client) SetUserAsActiveContext(ctx context.Context) error { func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
_, err := userRequest(ctx, "users.setActive", values, api.debug)
_, err = userRequest(ctx, api.httpclient, "users.setActive", values, api.debug)
return err return err
} }
@ -211,15 +385,12 @@ func (api *Client) SetUserPresence(presence string) error {
// SetUserPresenceContext changes the currently authenticated user presence with a custom context // SetUserPresenceContext changes the currently authenticated user presence with a custom context
func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) error { func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) error {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"presence": {presence}, "presence": {presence},
} }
_, err := userRequest(ctx, "users.setPresence", values, api.debug)
if err != nil {
return err
}
return nil
_, err := userRequest(ctx, api.httpclient, "users.setPresence", values, api.debug)
return err
} }
// GetUserIdentity will retrieve user info available per identity scopes // GetUserIdentity will retrieve user info available per identity scopes
@ -230,10 +401,11 @@ func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) {
// GetUserIdentityContext will retrieve user info available per identity scopes with a custom context // GetUserIdentityContext will retrieve user info available per identity scopes with a custom context
func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityResponse, error) { func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityResponse, error) {
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
response := &UserIdentityResponse{} response := &UserIdentityResponse{}
err := post(ctx, "users.identity", values, response, api.debug)
err := postForm(ctx, api.httpclient, SLACK_API+"users.identity", values, response, api.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -252,25 +424,24 @@ func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error {
func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) error { func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) error {
response := &SlackResponse{} response := &SlackResponse{}
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
if params.CropX != DEFAULT_USER_PHOTO_CROP_X { if params.CropX != DEFAULT_USER_PHOTO_CROP_X {
values.Add("crop_x", string(params.CropX)) values.Add("crop_x", strconv.Itoa(params.CropX))
} }
if params.CropY != DEFAULT_USER_PHOTO_CROP_Y { if params.CropY != DEFAULT_USER_PHOTO_CROP_Y {
values.Add("crop_y", string(params.CropY)) values.Add("crop_y", strconv.Itoa(params.CropX))
} }
if params.CropW != DEFAULT_USER_PHOTO_CROP_W { if params.CropW != DEFAULT_USER_PHOTO_CROP_W {
values.Add("crop_w", string(params.CropW)) values.Add("crop_w", strconv.Itoa(params.CropW))
} }
err := postLocalWithMultipartResponse(ctx, "users.setPhoto", image, "image", values, response, api.debug)
err := postLocalWithMultipartResponse(ctx, api.httpclient, "users.setPhoto", image, "image", values, response, api.debug)
if err != nil { if err != nil {
return err return err
} }
if !response.Ok {
return errors.New(response.Error) return response.Err()
}
return nil
} }
// DeleteUserPhoto deletes the current authenticated user's profile image // DeleteUserPhoto deletes the current authenticated user's profile image
@ -282,16 +453,15 @@ func (api *Client) DeleteUserPhoto() error {
func (api *Client) DeleteUserPhotoContext(ctx context.Context) error { func (api *Client) DeleteUserPhotoContext(ctx context.Context) error {
response := &SlackResponse{} response := &SlackResponse{}
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
} }
err := post(ctx, "users.deletePhoto", values, response, api.debug)
err := postForm(ctx, api.httpclient, SLACK_API+"users.deletePhoto", values, response, api.debug)
if err != nil { if err != nil {
return err return err
} }
if !response.Ok {
return errors.New(response.Error) return response.Err()
}
return nil
} }
// SetUserCustomStatus will set a custom status and emoji for the currently // SetUserCustomStatus will set a custom status and emoji for the currently
@ -331,13 +501,12 @@ func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, s
} }
values := url.Values{ values := url.Values{
"token": {api.config.token}, "token": {api.token},
"profile": {string(profile)}, "profile": {string(profile)},
} }
response := &userResponseFull{} response := &userResponseFull{}
if err = postForm(ctx, api.httpclient, SLACK_API+"users.profile.set", values, response, api.debug); err != nil {
if err = post(ctx, "users.profile.set", values, response, api.debug); err != nil {
return err return err
} }
@ -359,3 +528,31 @@ func (api *Client) UnsetUserCustomStatus() error {
func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error { func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error {
return api.SetUserCustomStatusContext(ctx, "", "") return api.SetUserCustomStatusContext(ctx, "", "")
} }
// GetUserProfile retrieves a user's profile information.
func (api *Client) GetUserProfile(userID string, includeLabels bool) (*UserProfile, error) {
return api.GetUserProfileContext(context.Background(), userID, includeLabels)
}
type getUserProfileResponse struct {
SlackResponse
Profile *UserProfile `json:"profile"`
}
// GetUserProfileContext retrieves a user's profile information with a context.
func (api *Client) GetUserProfileContext(ctx context.Context, userID string, includeLabels bool) (*UserProfile, error) {
values := url.Values{"token": {api.token}, "user": {userID}}
if includeLabels {
values.Add("include_labels", "true")
}
resp := &getUserProfileResponse{}
err := postSlackMethod(ctx, api.httpclient, "users.profile.get", values, &resp, api.debug)
if err != nil {
return nil, err
}
if !resp.Ok {
return nil, errors.New(resp.Error)
}
return resp.Profile, nil
}

33
vendor/github.com/nlopes/slack/webhooks.go generated vendored Normal file
View File

@ -0,0 +1,33 @@
package slack
import (
"github.com/pkg/errors"
"net/http"
"bytes"
"encoding/json"
)
type WebhookMessage struct {
Text string `json:"text,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
}
func PostWebhook(url string, 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));
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
}

View File

@ -3,9 +3,10 @@ package slack
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"sync"
"time" "time"
"golang.org/x/net/websocket" "github.com/gorilla/websocket"
) )
const ( const (
@ -19,8 +20,9 @@ const (
// //
// Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions) // Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions)
type RTM struct { type RTM struct {
idGen IDGenerator idGen IDGenerator
pings map[int]time.Time pingInterval time.Duration
pingDeadman *time.Timer
// Connection life-cycle // Connection life-cycle
conn *websocket.Conn conn *websocket.Conn
@ -44,6 +46,13 @@ type RTM struct {
// rtm.start to connect to Slack, otherwise it will use // rtm.start to connect to Slack, otherwise it will use
// rtm.connect // rtm.connect
useRTMStart bool useRTMStart bool
// dialer is a gorilla/websocket Dialer. If nil, use the default
// Dialer.
dialer *websocket.Dialer
// mu is mutex used to prevent RTM connection race conditions
mu *sync.Mutex
} }
// RTMOptions allows configuration of various options available for RTM messaging // RTMOptions allows configuration of various options available for RTM messaging
@ -60,9 +69,17 @@ type RTMOptions struct {
// Disconnect and wait, blocking until a successful disconnection. // Disconnect and wait, blocking until a successful disconnection.
func (rtm *RTM) Disconnect() error { func (rtm *RTM) Disconnect() error {
// this channel is always closed on disconnect. lets the ManagedConnection() function // avoid RTM disconnect race conditions
// properly clean up. rtm.mu.Lock()
close(rtm.disconnected) defer rtm.mu.Unlock()
// always push into the disconnected 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:
}
if !rtm.isConnected { if !rtm.isConnected {
return errors.New("Invalid call to Disconnect - Slack API is already disconnected") return errors.New("Invalid call to Disconnect - Slack API is already disconnected")
@ -72,12 +89,6 @@ func (rtm *RTM) Disconnect() error {
return nil return nil
} }
// Reconnect only makes sense if you've successfully disconnectd with Disconnect().
func (rtm *RTM) Reconnect() error {
logger.Println("RTM::Reconnect not implemented!")
return nil
}
// GetInfo returns the info structure received when calling // GetInfo returns the info structure received when calling
// "startrtm", holding all channels, groups and other metadata needed // "startrtm", holding all channels, groups and other metadata needed
// to implement a full chat client. It will be non-nil after a call to // to implement a full chat client. It will be non-nil after a call to
@ -97,3 +108,11 @@ func (rtm *RTM) SendMessage(msg *OutgoingMessage) {
rtm.outgoingMessages <- *msg rtm.outgoingMessages <- *msg
} }
func (rtm *RTM) resetDeadman() {
timerReset(rtm.pingDeadman, deadmanDuration(rtm.pingInterval))
}
func deadmanDuration(d time.Duration) time.Duration {
return d * 4
}

View File

@ -63,6 +63,13 @@ func (m *MessageTooLongEvent) Error() string {
return fmt.Sprintf("Message too long (max %d characters)", m.MaxLength) return fmt.Sprintf("Message too long (max %d characters)", m.MaxLength)
} }
// RateLimitEvent is used when Slack warns that rate-limits are being hit.
type RateLimitEvent struct{}
func (e *RateLimitEvent) Error() string {
return "Messages are being sent too fast."
}
// OutgoingErrorEvent contains information in case there were errors sending messages // OutgoingErrorEvent contains information in case there were errors sending messages
type OutgoingErrorEvent struct { type OutgoingErrorEvent struct {
Message OutgoingMessage Message OutgoingMessage

View File

@ -4,10 +4,11 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/http"
"reflect" "reflect"
"time" "time"
"golang.org/x/net/websocket" "github.com/gorilla/websocket"
) )
// ManageConnection can be called on a Slack RTM instance returned by the // ManageConnection can be called on a Slack RTM instance returned by the
@ -24,25 +25,35 @@ import (
// //
// The defined error events are located in websocket_internals.go. // The defined error events are located in websocket_internals.go.
func (rtm *RTM) ManageConnection() { func (rtm *RTM) ManageConnection() {
var connectionCount int var (
for { err error
connectionCount++ info *Info
conn *websocket.Conn
)
for connectionCount := 0; ; connectionCount++ {
// start trying to connect // start trying to connect
// the returned err is already passed onto the IncomingEvents channel // the returned err is already passed onto the IncomingEvents channel
info, conn, err := rtm.connect(connectionCount, rtm.useRTMStart) if info, conn, err = rtm.connect(connectionCount, rtm.useRTMStart); err != nil {
// if err != nil then the connection is sucessful - otherwise it is // when the connection is unsuccessful its fatal, and we need to bail out.
// fatal rtm.Debugf("Failed to connect with RTM on try %d: %s", connectionCount, err)
if err != nil {
return return
} }
// lock to prevent data races with Disconnect particularly around isConnected
// and conn.
rtm.mu.Lock()
rtm.conn = conn
rtm.isConnected = true
rtm.info = info rtm.info = info
rtm.mu.Unlock()
rtm.IncomingEvents <- RTMEvent{"connected", &ConnectedEvent{ rtm.IncomingEvents <- RTMEvent{"connected", &ConnectedEvent{
ConnectionCount: connectionCount, ConnectionCount: connectionCount,
Info: info, Info: info,
}} }}
rtm.conn = conn rtm.Debugf("RTM connection succeeded on try %d", connectionCount)
rtm.isConnected = true
keepRunning := make(chan bool) keepRunning := make(chan bool)
// we're now connected (or have failed fatally) so we can set up // we're now connected (or have failed fatally) so we can set up
@ -50,7 +61,7 @@ func (rtm *RTM) ManageConnection() {
go rtm.handleIncomingEvents(keepRunning) go rtm.handleIncomingEvents(keepRunning)
// this should be a blocking call until the connection has ended // this should be a blocking call until the connection has ended
rtm.handleEvents(keepRunning, 30*time.Second) rtm.handleEvents(keepRunning)
// after being disconnected we need to check if it was intentional // after being disconnected we need to check if it was intentional
// if not then we should try to reconnect // if not then we should try to reconnect
@ -67,6 +78,12 @@ func (rtm *RTM) ManageConnection() {
// If useRTMStart is false then it uses rtm.connect to create the connection, // If useRTMStart is false then it uses rtm.connect to create the connection,
// otherwise it uses rtm.start. // otherwise it uses rtm.start.
func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocket.Conn, error) { func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocket.Conn, error) {
const (
errInvalidAuth = "invalid_auth"
errInactiveAccount = "account_inactive"
errMissingAuthToken = "not_authed"
)
// used to provide exponential backoff wait time with jitter before trying // used to provide exponential backoff wait time with jitter before trying
// to connect to slack again // to connect to slack again
boff := &backoff{ boff := &backoff{
@ -87,10 +104,14 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke
if err == nil { if err == nil {
return info, conn, nil return info, conn, nil
} }
// check for fatal errors - currently only invalid_auth
if sErr, ok := err.(*WebError); ok && (sErr.Error() == "invalid_auth" || sErr.Error() == "account_inactive") { // check for fatal errors
switch err.Error() {
case errInvalidAuth, errInactiveAccount, errMissingAuthToken:
rtm.Debugf("Invalid auth when connecting with RTM: %s", err)
rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}} rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}}
return nil, nil, sErr return nil, nil, err
default:
} }
// any other errors are treated as recoverable and we try again after // any other errors are treated as recoverable and we try again after
@ -102,7 +123,7 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke
// check if Disconnect() has been invoked. // check if Disconnect() has been invoked.
select { select {
case _ = <-rtm.disconnected: case <-rtm.disconnected:
rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: true}} rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: true}}
return nil, nil, fmt.Errorf("disconnect received while trying to connect") return nil, nil, fmt.Errorf("disconnect received while trying to connect")
default: default:
@ -119,23 +140,34 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke
// startRTMAndDial attempts to connect to the slack websocket. If useRTMStart is true, // startRTMAndDial attempts to connect to the slack websocket. If useRTMStart is true,
// then it returns the full information returned by the "rtm.start" method on the // then it returns the full information returned by the "rtm.start" method on the
// slack API. Else it uses the "rtm.connect" method to connect // slack API. Else it uses the "rtm.connect" method to connect
func (rtm *RTM) startRTMAndDial(useRTMStart bool) (*Info, *websocket.Conn, error) { func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn, err error) {
var info *Info var (
var url string url string
var err error )
if useRTMStart { if useRTMStart {
rtm.Debugf("Starting RTM")
info, url, err = rtm.StartRTM() info, url, err = rtm.StartRTM()
} else { } else {
rtm.Debugf("Connecting to RTM")
info, url, err = rtm.ConnectRTM() info, url, err = rtm.ConnectRTM()
} }
if err != nil { if err != nil {
rtm.Debugf("Failed to start or connect to RTM: %s", err)
return nil, nil, err return nil, nil, err
} }
rtm.Debugf("Dialing to websocket on url %s", url)
// Only use HTTPS for connections to prevent MITM attacks on the connection. // Only use HTTPS for connections to prevent MITM attacks on the connection.
conn, err := websocketProxyDial(url, "https://api.slack.com") upgradeHeader := http.Header{}
upgradeHeader.Add("Origin", "https://api.slack.com")
dialer := websocket.DefaultDialer
if rtm.dialer != nil {
dialer = rtm.dialer
}
conn, _, err := dialer.Dial(url, upgradeHeader)
if err != nil { if err != nil {
rtm.Debugf("Failed to dial to the websocket: %s", err)
return nil, nil, err return nil, nil, err
} }
return info, conn, err return info, conn, err
@ -163,8 +195,8 @@ func (rtm *RTM) killConnection(keepRunning chan bool, intentional bool) error {
// interval. This also sends outgoing messages that are received from the RTM's // interval. This also sends outgoing messages that are received from the RTM's
// outgoingMessages channel. This also handles incoming raw events from the RTM // outgoingMessages channel. This also handles incoming raw events from the RTM
// rawEvents channel. // rawEvents channel.
func (rtm *RTM) handleEvents(keepRunning chan bool, interval time.Duration) { func (rtm *RTM) handleEvents(keepRunning chan bool) {
ticker := time.NewTicker(interval) ticker := time.NewTicker(rtm.pingInterval)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
@ -172,7 +204,12 @@ func (rtm *RTM) handleEvents(keepRunning chan bool, interval time.Duration) {
case intentional := <-rtm.killChannel: case intentional := <-rtm.killChannel:
_ = rtm.killConnection(keepRunning, intentional) _ = rtm.killConnection(keepRunning, intentional)
return return
// send pings on ticker interval
// detect when the connection is dead.
case <-rtm.pingDeadman.C:
rtm.Debugln("deadman switch trigger disconnecting")
_ = rtm.killConnection(keepRunning, false)
// send pings on ticker interval
case <-ticker.C: case <-ticker.C:
err := rtm.ping() err := rtm.ping()
if err != nil { if err != nil {
@ -190,7 +227,11 @@ func (rtm *RTM) handleEvents(keepRunning chan bool, interval time.Duration) {
rtm.sendOutgoingMessage(msg) rtm.sendOutgoingMessage(msg)
// listen for incoming messages that need to be parsed // listen for incoming messages that need to be parsed
case rawEvent := <-rtm.rawEvents: case rawEvent := <-rtm.rawEvents:
rtm.handleRawEvent(rawEvent) switch rtm.handleRawEvent(rawEvent) {
case rtmEventTypeGoodbye:
_ = rtm.killConnection(keepRunning, false)
default:
}
} }
} }
} }
@ -208,7 +249,9 @@ func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) {
case <-keepRunning: case <-keepRunning:
return return
default: default:
rtm.receiveIncomingEvent() if err := rtm.receiveIncomingEvent(); err != nil {
return
}
} }
} }
} }
@ -218,7 +261,7 @@ func (rtm *RTM) sendWithDeadline(msg interface{}) error {
if err := rtm.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { if err := rtm.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil {
return err return err
} }
if err := websocket.JSON.Send(rtm.conn, msg); err != nil { if err := rtm.conn.WriteJSON(msg); err != nil {
return err return err
} }
// remove write deadline // remove write deadline
@ -258,9 +301,7 @@ func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) {
func (rtm *RTM) ping() error { func (rtm *RTM) ping() error {
id := rtm.idGen.Next() id := rtm.idGen.Next()
rtm.Debugln("Sending PING ", id) rtm.Debugln("Sending PING ", id)
rtm.pings[id] = time.Now() msg := &Ping{ID: id, Type: "ping", Timestamp: time.Now().Unix()}
msg := &Ping{ID: id, Type: "ping"}
if err := rtm.sendWithDeadline(msg); err != nil { if err := rtm.sendWithDeadline(msg); err != nil {
rtm.Debugf("RTM Error sending 'PING %d': %s", id, err.Error()) rtm.Debugf("RTM Error sending 'PING %d': %s", id, err.Error())
@ -271,52 +312,62 @@ func (rtm *RTM) ping() error {
// receiveIncomingEvent attempts to receive an event from the RTM's websocket. // receiveIncomingEvent attempts to receive an event from the RTM's websocket.
// This will block until a frame is available from the websocket. // This will block until a frame is available from the websocket.
func (rtm *RTM) receiveIncomingEvent() { // If the read from the websocket results in a fatal error, this function will return non-nil.
func (rtm *RTM) receiveIncomingEvent() error {
event := json.RawMessage{} event := json.RawMessage{}
err := websocket.JSON.Receive(rtm.conn, &event) err := rtm.conn.ReadJSON(&event)
if err == io.EOF { switch {
case err == io.ErrUnexpectedEOF:
// EOF's don't seem to signify a failed connection so instead we ignore // EOF's don't seem to signify a failed connection so instead we ignore
// them here and detect a failed connection upon attempting to send a // them here and detect a failed connection upon attempting to send a
// 'PING' message // 'PING' message
// trigger a 'PING' to detect pontential websocket disconnect // trigger a 'PING' to detect potential websocket disconnect
rtm.forcePing <- true rtm.forcePing <- true
return case err != nil:
} else if 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{ rtm.IncomingEvents <- RTMEvent{"incoming_error", &IncomingEventError{
ErrorObj: err, ErrorObj: err,
}} }}
// force a ping here too? rtm.killChannel <- false
return return err
} else if len(event) == 0 { case len(event) == 0:
rtm.Debugln("Received empty event") rtm.Debugln("Received empty event")
return default:
rtm.Debugln("Incoming Event:", string(event[:]))
rtm.rawEvents <- event
} }
rtm.Debugln("Incoming Event:", string(event[:])) return nil
rtm.rawEvents <- event
} }
// handleRawEvent takes a raw JSON message received from the slack websocket // handleRawEvent takes a raw JSON message received from the slack websocket
// and handles the encoded event. // and handles the encoded event.
func (rtm *RTM) handleRawEvent(rawEvent json.RawMessage) { // returns the event type of the message.
func (rtm *RTM) handleRawEvent(rawEvent json.RawMessage) string {
event := &Event{} event := &Event{}
err := json.Unmarshal(rawEvent, event) err := json.Unmarshal(rawEvent, event)
if err != nil { if err != nil {
rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}}
return return ""
} }
switch event.Type { switch event.Type {
case "": case rtmEventTypeAck:
rtm.handleAck(rawEvent) rtm.handleAck(rawEvent)
case "hello": case rtmEventTypeHello:
rtm.IncomingEvents <- RTMEvent{"hello", &HelloEvent{}} rtm.IncomingEvents <- RTMEvent{"hello", &HelloEvent{}}
case "pong": case rtmEventTypePong:
rtm.handlePong(rawEvent) rtm.handlePong(rawEvent)
case "desktop_notification": case rtmEventTypeGoodbye:
// just return the event type up for goodbye, will be handled by caller.
case rtmEventTypeDesktopNotification:
rtm.Debugln("Received desktop notification, ignoring") rtm.Debugln("Received desktop notification, ignoring")
default: default:
rtm.handleEvent(event.Type, rawEvent) rtm.handleEvent(event.Type, rawEvent)
} }
return event.Type
} }
// handleAck handles an incoming 'ACK' message. // handleAck handles an incoming 'ACK' message.
@ -331,7 +382,13 @@ func (rtm *RTM) handleAck(event json.RawMessage) {
if ack.Ok { if ack.Ok {
rtm.IncomingEvents <- RTMEvent{"ack", ack} rtm.IncomingEvents <- RTMEvent{"ack", ack}
} else if ack.RTMResponse.Error != nil { } else if ack.RTMResponse.Error != nil {
rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error}} // As there is no documentation for RTM error-codes, this
// identification of a rate-limit warning is very brittle.
if ack.RTMResponse.Error.Code == -1 && ack.RTMResponse.Error.Msg == "slow down, too many messages..." {
rtm.IncomingEvents <- RTMEvent{"ack_error", &RateLimitEvent{}}
} else {
rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error}}
}
} else { } else {
rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{fmt.Errorf("ack decode failure")}} rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{fmt.Errorf("ack decode failure")}}
} }
@ -341,19 +398,20 @@ func (rtm *RTM) handleAck(event json.RawMessage) {
// a previously sent 'PING' message. This is then used to compute the // a previously sent 'PING' message. This is then used to compute the
// connection's latency. // connection's latency.
func (rtm *RTM) handlePong(event json.RawMessage) { func (rtm *RTM) handlePong(event json.RawMessage) {
pong := &Pong{} var (
if err := json.Unmarshal(event, pong); err != nil { p Pong
rtm.Debugln("RTM Error unmarshalling 'pong' event:", err) )
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.Debugln(" -> Erroneous 'ping' event:", string(event))
return return
} }
if pingTime, exists := rtm.pings[pong.ReplyTo]; exists {
latency := time.Since(pingTime) latency := time.Since(time.Unix(p.Timestamp, 0))
rtm.IncomingEvents <- RTMEvent{"latency_report", &LatencyReport{Value: latency}} rtm.IncomingEvents <- RTMEvent{"latency_report", &LatencyReport{Value: latency}}
delete(rtm.pings, pong.ReplyTo)
} else {
rtm.Debugln("RTM Error - unmatched 'pong' event:", string(event))
}
} }
// handleEvent is the "default" response to an event that does not have a // handleEvent is the "default" response to an event that does not have a
@ -363,7 +421,7 @@ func (rtm *RTM) handlePong(event json.RawMessage) {
// correct struct then this sends an UnmarshallingErrorEvent to the // correct struct then this sends an UnmarshallingErrorEvent to the
// IncomingEvents channel. // IncomingEvents channel.
func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) {
v, exists := eventMapping[typeStr] v, exists := EventMapping[typeStr]
if !exists { if !exists {
rtm.Debugf("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\n", typeStr, string(event)) err := fmt.Errorf("RTM Error: Received unmapped event %q: %s\n", typeStr, string(event))
@ -382,10 +440,10 @@ func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) {
rtm.IncomingEvents <- RTMEvent{typeStr, recvEvent} rtm.IncomingEvents <- RTMEvent{typeStr, recvEvent}
} }
// eventMapping holds a mapping of event names to their corresponding struct // EventMapping holds a mapping of event names to their corresponding struct
// implementations. The structs should be instances of the unmarshalling // implementations. The structs should be instances of the unmarshalling
// target for the matching event type. // target for the matching event type.
var eventMapping = map[string]interface{}{ var EventMapping = map[string]interface{}{
"message": MessageEvent{}, "message": MessageEvent{},
"presence_change": PresenceChangeEvent{}, "presence_change": PresenceChangeEvent{},
"user_typing": UserTypingEvent{}, "user_typing": UserTypingEvent{},
@ -463,4 +521,7 @@ var eventMapping = map[string]interface{}{
"accounts_changed": AccountsChangedEvent{}, "accounts_changed": AccountsChangedEvent{},
"reconnect_url": ReconnectUrlEvent{}, "reconnect_url": ReconnectUrlEvent{},
"member_joined_channel": MemberJoinedChannelEvent{},
"member_left_channel": MemberLeftChannelEvent{},
} }

View File

@ -80,7 +80,7 @@ type EmojiChangedEvent struct {
SubType string `json:"subtype"` SubType string `json:"subtype"`
Name string `json:"name"` Name string `json:"name"`
Names []string `json:"names"` Names []string `json:"names"`
Value string `json:"value"` Value string `json:"value"`
EventTimestamp string `json:"event_ts"` EventTimestamp string `json:"event_ts"`
} }
@ -119,3 +119,22 @@ type ReconnectUrlEvent struct {
Type string `json:"type"` Type string `json:"type"`
URL string `json:"url"` URL string `json:"url"`
} }
// MemberJoinedChannelEvent, a user joined a public or private channel
type MemberJoinedChannelEvent struct {
Type string `json:"type"`
User string `json:"user"`
Channel string `json:"channel"`
ChannelType string `json:"channel_type"`
Team string `json:"team"`
Inviter string `json:"inviter"`
}
// MemberJoinedChannelEvent, a user left a public or private channel
type MemberLeftChannelEvent struct {
Type string `json:"type"`
User string `json:"user"`
Channel string `json:"channel"`
ChannelType string `json:"channel_type"`
Team string `json:"team"`
}

View File

@ -1,82 +0,0 @@
package slack
import (
"crypto/tls"
"errors"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"golang.org/x/net/websocket"
)
// Taken and reworked from: https://gist.github.com/madmo/8548738
func websocketHTTPConnect(proxy, urlString string) (net.Conn, error) {
p, err := net.Dial("tcp", proxy)
if err != nil {
return nil, err
}
turl, err := url.Parse(urlString)
if err != nil {
return nil, err
}
req := http.Request{
Method: "CONNECT",
URL: &url.URL{},
Host: turl.Host,
}
cc := httputil.NewProxyClientConn(p, nil)
if _, err := cc.Do(&req); err != nil {
return nil, err
}
rwc, _ := cc.Hijack()
return rwc, nil
}
func websocketProxyDial(urlString, origin string) (ws *websocket.Conn, err error) {
if os.Getenv("HTTP_PROXY") == "" {
return websocket.Dial(urlString, "", origin)
}
purl, err := url.Parse(os.Getenv("HTTP_PROXY"))
if err != nil {
return nil, err
}
config, err := websocket.NewConfig(urlString, origin)
if err != nil {
return nil, err
}
client, err := websocketHTTPConnect(purl.Host, urlString)
if err != nil {
return nil, err
}
switch config.Location.Scheme {
case "ws":
case "wss":
tlsClient := tls.Client(client, &tls.Config{
ServerName: strings.Split(config.Location.Host, ":")[0],
})
err := tlsClient.Handshake()
if err != nil {
tlsClient.Close()
return nil, err
}
client = tlsClient
default:
return nil, errors.New("invalid websocket schema")
}
return websocket.NewClient(config, client)
}

24
vendor/github.com/pkg/errors/.gitignore generated vendored Normal file
View File

@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

11
vendor/github.com/pkg/errors/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,11 @@
language: go
go_import_path: github.com/pkg/errors
go:
- 1.4.3
- 1.5.4
- 1.6.2
- 1.7.1
- tip
script:
- go test -v ./...

23
vendor/github.com/pkg/errors/LICENSE generated vendored Normal file
View File

@ -0,0 +1,23 @@
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

52
vendor/github.com/pkg/errors/README.md generated vendored Normal file
View File

@ -0,0 +1,52 @@
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors)
Package errors provides simple error handling primitives.
`go get github.com/pkg/errors`
The traditional error handling idiom in Go is roughly akin to
```go
if err != nil {
return err
}
```
which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
## Adding context to an error
The errors.Wrap function returns a new error that adds context to the original error. For example
```go
_, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrap(err, "read failed")
}
```
## Retrieving the cause of an error
Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
```go
type causer interface {
Cause() error
}
```
`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
```go
switch err := errors.Cause(err).(type) {
case *MyError:
// handle specifically
default:
// unknown error
}
```
[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).
## Contributing
We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high.
Before proposing a change, please discuss your change by raising an issue.
## Licence
BSD-2-Clause

32
vendor/github.com/pkg/errors/appveyor.yml generated vendored Normal file
View File

@ -0,0 +1,32 @@
version: build-{build}.{branch}
clone_folder: C:\gopath\src\github.com\pkg\errors
shallow_clone: true # for startup speed
environment:
GOPATH: C:\gopath
platform:
- x64
# http://www.appveyor.com/docs/installed-software
install:
# some helpful output for debugging builds
- go version
- go env
# pre-installed MinGW at C:\MinGW is 32bit only
# but MSYS2 at C:\msys64 has mingw64
- set PATH=C:\msys64\mingw64\bin;%PATH%
- gcc --version
- g++ --version
build_script:
- go install -v ./...
test_script:
- set PATH=C:\gopath\bin;%PATH%
- go test -v ./...
#artifacts:
# - path: '%GOPATH%\bin\*.exe'
deploy: off

269
vendor/github.com/pkg/errors/errors.go generated vendored Normal file
View File

@ -0,0 +1,269 @@
// Package errors provides simple error handling primitives.
//
// The traditional error handling idiom in Go is roughly akin to
//
// if err != nil {
// return err
// }
//
// which applied recursively up the call stack results in error reports
// without context or debugging information. The errors package allows
// programmers to add context to the failure path in their code in a way
// that does not destroy the original value of the error.
//
// Adding context to an error
//
// The errors.Wrap function returns a new error that adds context to the
// original error by recording a stack trace at the point Wrap is called,
// and the supplied message. For example
//
// _, err := ioutil.ReadAll(r)
// if err != nil {
// return errors.Wrap(err, "read failed")
// }
//
// If additional control is required the errors.WithStack and errors.WithMessage
// functions destructure errors.Wrap into its component operations of annotating
// an error with a stack trace and an a message, respectively.
//
// Retrieving the cause of an error
//
// Using errors.Wrap constructs a stack of errors, adding context to the
// preceding error. Depending on the nature of the error it may be necessary
// to reverse the operation of errors.Wrap to retrieve the original error
// for inspection. Any error value which implements this interface
//
// type causer interface {
// Cause() error
// }
//
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
// the topmost error which does not implement causer, which is assumed to be
// the original cause. For example:
//
// switch err := errors.Cause(err).(type) {
// case *MyError:
// // handle specifically
// default:
// // unknown error
// }
//
// causer interface is not exported by this package, but is considered a part
// of stable public API.
//
// Formatted printing of errors
//
// All error values returned from this package implement fmt.Formatter and can
// be formatted by the fmt package. The following verbs are supported
//
// %s print the error. If the error has a Cause it will be
// printed recursively
// %v see %s
// %+v extended format. Each Frame of the error's StackTrace will
// be printed in detail.
//
// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface.
//
// type stackTracer interface {
// StackTrace() errors.StackTrace
// }
//
// Where errors.StackTrace is defined as
//
// type StackTrace []Frame
//
// The Frame type represents a call site in the stack trace. Frame supports
// the fmt.Formatter interface that can be used for printing information about
// the stack trace of this error. For example:
//
// if err, ok := err.(stackTracer); ok {
// for _, f := range err.StackTrace() {
// fmt.Printf("%+s:%d", f)
// }
// }
//
// stackTracer interface is not exported by this package, but is considered a part
// of stable public API.
//
// See the documentation for Frame.Format for more details.
package errors
import (
"fmt"
"io"
)
// New returns an error with the supplied message.
// New also records the stack trace at the point it was called.
func New(message string) error {
return &fundamental{
msg: message,
stack: callers(),
}
}
// Errorf formats according to a format specifier and returns the string
// as a value that satisfies error.
// Errorf also records the stack trace at the point it was called.
func Errorf(format string, args ...interface{}) error {
return &fundamental{
msg: fmt.Sprintf(format, args...),
stack: callers(),
}
}
// fundamental is an error that has a message and a stack, but no caller.
type fundamental struct {
msg string
*stack
}
func (f *fundamental) Error() string { return f.msg }
func (f *fundamental) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
io.WriteString(s, f.msg)
f.stack.Format(s, verb)
return
}
fallthrough
case 's':
io.WriteString(s, f.msg)
case 'q':
fmt.Fprintf(s, "%q", f.msg)
}
}
// WithStack annotates err with a stack trace at the point WithStack was called.
// If err is nil, WithStack returns nil.
func WithStack(err error) error {
if err == nil {
return nil
}
return &withStack{
err,
callers(),
}
}
type withStack struct {
error
*stack
}
func (w *withStack) Cause() error { return w.error }
func (w *withStack) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
fmt.Fprintf(s, "%+v", w.Cause())
w.stack.Format(s, verb)
return
}
fallthrough
case 's':
io.WriteString(s, w.Error())
case 'q':
fmt.Fprintf(s, "%q", w.Error())
}
}
// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: message,
}
return &withStack{
err,
callers(),
}
}
// Wrapf returns an error annotating err with a stack trace
// at the point Wrapf is call, and the format specifier.
// If err is nil, Wrapf returns nil.
func Wrapf(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: fmt.Sprintf(format, args...),
}
return &withStack{
err,
callers(),
}
}
// WithMessage annotates err with a new message.
// If err is nil, WithMessage returns nil.
func WithMessage(err error, message string) error {
if err == nil {
return nil
}
return &withMessage{
cause: err,
msg: message,
}
}
type withMessage struct {
cause error
msg string
}
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
func (w *withMessage) Cause() error { return w.cause }
func (w *withMessage) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
fmt.Fprintf(s, "%+v\n", w.Cause())
io.WriteString(s, w.msg)
return
}
fallthrough
case 's', 'q':
io.WriteString(s, w.Error())
}
}
// Cause returns the underlying cause of the error, if possible.
// An error value has a cause if it implements the following
// interface:
//
// type causer interface {
// Cause() error
// }
//
// If the error does not implement Cause, the original error will
// be returned. If the error is nil, nil will be returned without further
// investigation.
func Cause(err error) error {
type causer interface {
Cause() error
}
for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
return err
}

178
vendor/github.com/pkg/errors/stack.go generated vendored Normal file
View File

@ -0,0 +1,178 @@
package errors
import (
"fmt"
"io"
"path"
"runtime"
"strings"
)
// Frame represents a program counter inside a stack frame.
type Frame uintptr
// pc returns the program counter for this frame;
// multiple frames may have the same PC value.
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
// file returns the full path to the file that contains the
// function for this Frame's pc.
func (f Frame) file() string {
fn := runtime.FuncForPC(f.pc())
if fn == nil {
return "unknown"
}
file, _ := fn.FileLine(f.pc())
return file
}
// line returns the line number of source code of the
// function for this Frame's pc.
func (f Frame) line() int {
fn := runtime.FuncForPC(f.pc())
if fn == nil {
return 0
}
_, line := fn.FileLine(f.pc())
return line
}
// Format formats the frame according to the fmt.Formatter interface.
//
// %s source file
// %d source line
// %n function name
// %v equivalent to %s:%d
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
// %+s path of source file relative to the compile time GOPATH
// %+v equivalent to %+s:%d
func (f Frame) Format(s fmt.State, verb rune) {
switch verb {
case 's':
switch {
case s.Flag('+'):
pc := f.pc()
fn := runtime.FuncForPC(pc)
if fn == nil {
io.WriteString(s, "unknown")
} else {
file, _ := fn.FileLine(pc)
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
}
default:
io.WriteString(s, path.Base(f.file()))
}
case 'd':
fmt.Fprintf(s, "%d", f.line())
case 'n':
name := runtime.FuncForPC(f.pc()).Name()
io.WriteString(s, funcname(name))
case 'v':
f.Format(s, 's')
io.WriteString(s, ":")
f.Format(s, 'd')
}
}
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
type StackTrace []Frame
func (st StackTrace) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
switch {
case s.Flag('+'):
for _, f := range st {
fmt.Fprintf(s, "\n%+v", f)
}
case s.Flag('#'):
fmt.Fprintf(s, "%#v", []Frame(st))
default:
fmt.Fprintf(s, "%v", []Frame(st))
}
case 's':
fmt.Fprintf(s, "%s", []Frame(st))
}
}
// stack represents a stack of program counters.
type stack []uintptr
func (s *stack) Format(st fmt.State, verb rune) {
switch verb {
case 'v':
switch {
case st.Flag('+'):
for _, pc := range *s {
f := Frame(pc)
fmt.Fprintf(st, "\n%+v", f)
}
}
}
}
func (s *stack) StackTrace() StackTrace {
f := make([]Frame, len(*s))
for i := 0; i < len(f); i++ {
f[i] = Frame((*s)[i])
}
return f
}
func callers() *stack {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
var st stack = pcs[0:n]
return &st
}
// funcname removes the path prefix component of a function's name reported by func.Name().
func funcname(name string) string {
i := strings.LastIndex(name, "/")
name = name[i+1:]
i = strings.Index(name, ".")
return name[i+1:]
}
func trimGOPATH(name, file string) string {
// Here we want to get the source file path relative to the compile time
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
// GOPATH at runtime, but we can infer the number of path segments in the
// GOPATH. We note that fn.Name() returns the function name qualified by
// the import path, which does not include the GOPATH. Thus we can trim
// segments from the beginning of the file path until the number of path
// separators remaining is one more than the number of path separators in
// the function name. For example, given:
//
// GOPATH /home/user
// file /home/user/src/pkg/sub/file.go
// fn.Name() pkg/sub.Type.Method
//
// We want to produce:
//
// pkg/sub/file.go
//
// From this we can easily see that fn.Name() has one less path separator
// than our desired output. We count separators from the end of the file
// path until it finds two more than in the function name and then move
// one character forward to preserve the initial path segment without a
// leading separator.
const sep = "/"
goal := strings.Count(name, sep) + 2
i := len(file)
for n := 0; n < goal; n++ {
i = strings.LastIndex(file[:i], sep)
if i == -1 {
// not enough separators found, set i so that the slice expression
// below leaves file unmodified
i = -len(sep)
break
}
}
// get back to 0 or trim the leading separator
file = file[i+len(sep):]
return file
}

3
vendor/golang.org/x/net/AUTHORS generated vendored
View File

@ -1,3 +0,0 @@
# This source code refers to The Go Authors for copyright purposes.
# The master list of authors is in the main Go distribution,
# visible at http://tip.golang.org/AUTHORS.

View File

@ -1,3 +0,0 @@
# This source code was written by the Go contributors.
# The master list of contributors is in the main Go distribution,
# visible at http://tip.golang.org/CONTRIBUTORS.

27
vendor/golang.org/x/net/LICENSE generated vendored
View File

@ -1,27 +0,0 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

22
vendor/golang.org/x/net/PATENTS generated vendored
View File

@ -1,22 +0,0 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in
the future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.

View File

@ -1,106 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bufio"
"io"
"net"
"net/http"
"net/url"
)
// DialError is an error that occurs while dialling a websocket server.
type DialError struct {
*Config
Err error
}
func (e *DialError) Error() string {
return "websocket.Dial " + e.Config.Location.String() + ": " + e.Err.Error()
}
// NewConfig creates a new WebSocket config for client connection.
func NewConfig(server, origin string) (config *Config, err error) {
config = new(Config)
config.Version = ProtocolVersionHybi13
config.Location, err = url.ParseRequestURI(server)
if err != nil {
return
}
config.Origin, err = url.ParseRequestURI(origin)
if err != nil {
return
}
config.Header = http.Header(make(map[string][]string))
return
}
// NewClient creates a new WebSocket client connection over rwc.
func NewClient(config *Config, rwc io.ReadWriteCloser) (ws *Conn, err error) {
br := bufio.NewReader(rwc)
bw := bufio.NewWriter(rwc)
err = hybiClientHandshake(config, br, bw)
if err != nil {
return
}
buf := bufio.NewReadWriter(br, bw)
ws = newHybiClientConn(config, buf, rwc)
return
}
// Dial opens a new client connection to a WebSocket.
func Dial(url_, protocol, origin string) (ws *Conn, err error) {
config, err := NewConfig(url_, origin)
if err != nil {
return nil, err
}
if protocol != "" {
config.Protocol = []string{protocol}
}
return DialConfig(config)
}
var portMap = map[string]string{
"ws": "80",
"wss": "443",
}
func parseAuthority(location *url.URL) string {
if _, ok := portMap[location.Scheme]; ok {
if _, _, err := net.SplitHostPort(location.Host); err != nil {
return net.JoinHostPort(location.Host, portMap[location.Scheme])
}
}
return location.Host
}
// DialConfig opens a new client connection to a WebSocket with a config.
func DialConfig(config *Config) (ws *Conn, err error) {
var client net.Conn
if config.Location == nil {
return nil, &DialError{config, ErrBadWebSocketLocation}
}
if config.Origin == nil {
return nil, &DialError{config, ErrBadWebSocketOrigin}
}
dialer := config.Dialer
if dialer == nil {
dialer = &net.Dialer{}
}
client, err = dialWithDialer(dialer, config)
if err != nil {
goto Error
}
ws, err = NewClient(config, client)
if err != nil {
client.Close()
goto Error
}
return
Error:
return nil, &DialError{config, err}
}

View File

@ -1,24 +0,0 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"crypto/tls"
"net"
)
func dialWithDialer(dialer *net.Dialer, config *Config) (conn net.Conn, err error) {
switch config.Location.Scheme {
case "ws":
conn, err = dialer.Dial("tcp", parseAuthority(config.Location))
case "wss":
conn, err = tls.DialWithDialer(dialer, "tcp", parseAuthority(config.Location), config.TlsConfig)
default:
err = ErrBadScheme
}
return
}

View File

@ -1,583 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
// This file implements a protocol of hybi draft.
// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
const (
websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
closeStatusNormal = 1000
closeStatusGoingAway = 1001
closeStatusProtocolError = 1002
closeStatusUnsupportedData = 1003
closeStatusFrameTooLarge = 1004
closeStatusNoStatusRcvd = 1005
closeStatusAbnormalClosure = 1006
closeStatusBadMessageData = 1007
closeStatusPolicyViolation = 1008
closeStatusTooBigData = 1009
closeStatusExtensionMismatch = 1010
maxControlFramePayloadLength = 125
)
var (
ErrBadMaskingKey = &ProtocolError{"bad masking key"}
ErrBadPongMessage = &ProtocolError{"bad pong message"}
ErrBadClosingStatus = &ProtocolError{"bad closing status"}
ErrUnsupportedExtensions = &ProtocolError{"unsupported extensions"}
ErrNotImplemented = &ProtocolError{"not implemented"}
handshakeHeader = map[string]bool{
"Host": true,
"Upgrade": true,
"Connection": true,
"Sec-Websocket-Key": true,
"Sec-Websocket-Origin": true,
"Sec-Websocket-Version": true,
"Sec-Websocket-Protocol": true,
"Sec-Websocket-Accept": true,
}
)
// A hybiFrameHeader is a frame header as defined in hybi draft.
type hybiFrameHeader struct {
Fin bool
Rsv [3]bool
OpCode byte
Length int64
MaskingKey []byte
data *bytes.Buffer
}
// A hybiFrameReader is a reader for hybi frame.
type hybiFrameReader struct {
reader io.Reader
header hybiFrameHeader
pos int64
length int
}
func (frame *hybiFrameReader) Read(msg []byte) (n int, err error) {
n, err = frame.reader.Read(msg)
if frame.header.MaskingKey != nil {
for i := 0; i < n; i++ {
msg[i] = msg[i] ^ frame.header.MaskingKey[frame.pos%4]
frame.pos++
}
}
return n, err
}
func (frame *hybiFrameReader) PayloadType() byte { return frame.header.OpCode }
func (frame *hybiFrameReader) HeaderReader() io.Reader {
if frame.header.data == nil {
return nil
}
if frame.header.data.Len() == 0 {
return nil
}
return frame.header.data
}
func (frame *hybiFrameReader) TrailerReader() io.Reader { return nil }
func (frame *hybiFrameReader) Len() (n int) { return frame.length }
// A hybiFrameReaderFactory creates new frame reader based on its frame type.
type hybiFrameReaderFactory struct {
*bufio.Reader
}
// NewFrameReader reads a frame header from the connection, and creates new reader for the frame.
// See Section 5.2 Base Framing protocol for detail.
// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17#section-5.2
func (buf hybiFrameReaderFactory) NewFrameReader() (frame frameReader, err error) {
hybiFrame := new(hybiFrameReader)
frame = hybiFrame
var header []byte
var b byte
// First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits)
b, err = buf.ReadByte()
if err != nil {
return
}
header = append(header, b)
hybiFrame.header.Fin = ((header[0] >> 7) & 1) != 0
for i := 0; i < 3; i++ {
j := uint(6 - i)
hybiFrame.header.Rsv[i] = ((header[0] >> j) & 1) != 0
}
hybiFrame.header.OpCode = header[0] & 0x0f
// Second byte. Mask/Payload len(7bits)
b, err = buf.ReadByte()
if err != nil {
return
}
header = append(header, b)
mask := (b & 0x80) != 0
b &= 0x7f
lengthFields := 0
switch {
case b <= 125: // Payload length 7bits.
hybiFrame.header.Length = int64(b)
case b == 126: // Payload length 7+16bits
lengthFields = 2
case b == 127: // Payload length 7+64bits
lengthFields = 8
}
for i := 0; i < lengthFields; i++ {
b, err = buf.ReadByte()
if err != nil {
return
}
if lengthFields == 8 && i == 0 { // MSB must be zero when 7+64 bits
b &= 0x7f
}
header = append(header, b)
hybiFrame.header.Length = hybiFrame.header.Length*256 + int64(b)
}
if mask {
// Masking key. 4 bytes.
for i := 0; i < 4; i++ {
b, err = buf.ReadByte()
if err != nil {
return
}
header = append(header, b)
hybiFrame.header.MaskingKey = append(hybiFrame.header.MaskingKey, b)
}
}
hybiFrame.reader = io.LimitReader(buf.Reader, hybiFrame.header.Length)
hybiFrame.header.data = bytes.NewBuffer(header)
hybiFrame.length = len(header) + int(hybiFrame.header.Length)
return
}
// A HybiFrameWriter is a writer for hybi frame.
type hybiFrameWriter struct {
writer *bufio.Writer
header *hybiFrameHeader
}
func (frame *hybiFrameWriter) Write(msg []byte) (n int, err error) {
var header []byte
var b byte
if frame.header.Fin {
b |= 0x80
}
for i := 0; i < 3; i++ {
if frame.header.Rsv[i] {
j := uint(6 - i)
b |= 1 << j
}
}
b |= frame.header.OpCode
header = append(header, b)
if frame.header.MaskingKey != nil {
b = 0x80
} else {
b = 0
}
lengthFields := 0
length := len(msg)
switch {
case length <= 125:
b |= byte(length)
case length < 65536:
b |= 126
lengthFields = 2
default:
b |= 127
lengthFields = 8
}
header = append(header, b)
for i := 0; i < lengthFields; i++ {
j := uint((lengthFields - i - 1) * 8)
b = byte((length >> j) & 0xff)
header = append(header, b)
}
if frame.header.MaskingKey != nil {
if len(frame.header.MaskingKey) != 4 {
return 0, ErrBadMaskingKey
}
header = append(header, frame.header.MaskingKey...)
frame.writer.Write(header)
data := make([]byte, length)
for i := range data {
data[i] = msg[i] ^ frame.header.MaskingKey[i%4]
}
frame.writer.Write(data)
err = frame.writer.Flush()
return length, err
}
frame.writer.Write(header)
frame.writer.Write(msg)
err = frame.writer.Flush()
return length, err
}
func (frame *hybiFrameWriter) Close() error { return nil }
type hybiFrameWriterFactory struct {
*bufio.Writer
needMaskingKey bool
}
func (buf hybiFrameWriterFactory) NewFrameWriter(payloadType byte) (frame frameWriter, err error) {
frameHeader := &hybiFrameHeader{Fin: true, OpCode: payloadType}
if buf.needMaskingKey {
frameHeader.MaskingKey, err = generateMaskingKey()
if err != nil {
return nil, err
}
}
return &hybiFrameWriter{writer: buf.Writer, header: frameHeader}, nil
}
type hybiFrameHandler struct {
conn *Conn
payloadType byte
}
func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (frameReader, error) {
if handler.conn.IsServerConn() {
// The client MUST mask all frames sent to the server.
if frame.(*hybiFrameReader).header.MaskingKey == nil {
handler.WriteClose(closeStatusProtocolError)
return nil, io.EOF
}
} else {
// The server MUST NOT mask all frames.
if frame.(*hybiFrameReader).header.MaskingKey != nil {
handler.WriteClose(closeStatusProtocolError)
return nil, io.EOF
}
}
if header := frame.HeaderReader(); header != nil {
io.Copy(ioutil.Discard, header)
}
switch frame.PayloadType() {
case ContinuationFrame:
frame.(*hybiFrameReader).header.OpCode = handler.payloadType
case TextFrame, BinaryFrame:
handler.payloadType = frame.PayloadType()
case CloseFrame:
return nil, io.EOF
case PingFrame, PongFrame:
b := make([]byte, maxControlFramePayloadLength)
n, err := io.ReadFull(frame, b)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return nil, err
}
io.Copy(ioutil.Discard, frame)
if frame.PayloadType() == PingFrame {
if _, err := handler.WritePong(b[:n]); err != nil {
return nil, err
}
}
return nil, nil
}
return frame, nil
}
func (handler *hybiFrameHandler) WriteClose(status int) (err error) {
handler.conn.wio.Lock()
defer handler.conn.wio.Unlock()
w, err := handler.conn.frameWriterFactory.NewFrameWriter(CloseFrame)
if err != nil {
return err
}
msg := make([]byte, 2)
binary.BigEndian.PutUint16(msg, uint16(status))
_, err = w.Write(msg)
w.Close()
return err
}
func (handler *hybiFrameHandler) WritePong(msg []byte) (n int, err error) {
handler.conn.wio.Lock()
defer handler.conn.wio.Unlock()
w, err := handler.conn.frameWriterFactory.NewFrameWriter(PongFrame)
if err != nil {
return 0, err
}
n, err = w.Write(msg)
w.Close()
return n, err
}
// newHybiConn creates a new WebSocket connection speaking hybi draft protocol.
func newHybiConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn {
if buf == nil {
br := bufio.NewReader(rwc)
bw := bufio.NewWriter(rwc)
buf = bufio.NewReadWriter(br, bw)
}
ws := &Conn{config: config, request: request, buf: buf, rwc: rwc,
frameReaderFactory: hybiFrameReaderFactory{buf.Reader},
frameWriterFactory: hybiFrameWriterFactory{
buf.Writer, request == nil},
PayloadType: TextFrame,
defaultCloseStatus: closeStatusNormal}
ws.frameHandler = &hybiFrameHandler{conn: ws}
return ws
}
// generateMaskingKey generates a masking key for a frame.
func generateMaskingKey() (maskingKey []byte, err error) {
maskingKey = make([]byte, 4)
if _, err = io.ReadFull(rand.Reader, maskingKey); err != nil {
return
}
return
}
// generateNonce generates a nonce consisting of a randomly selected 16-byte
// value that has been base64-encoded.
func generateNonce() (nonce []byte) {
key := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
panic(err)
}
nonce = make([]byte, 24)
base64.StdEncoding.Encode(nonce, key)
return
}
// removeZone removes IPv6 zone identifer from host.
// E.g., "[fe80::1%en0]:8080" to "[fe80::1]:8080"
func removeZone(host string) string {
if !strings.HasPrefix(host, "[") {
return host
}
i := strings.LastIndex(host, "]")
if i < 0 {
return host
}
j := strings.LastIndex(host[:i], "%")
if j < 0 {
return host
}
return host[:j] + host[i:]
}
// getNonceAccept computes the base64-encoded SHA-1 of the concatenation of
// the nonce ("Sec-WebSocket-Key" value) with the websocket GUID string.
func getNonceAccept(nonce []byte) (expected []byte, err error) {
h := sha1.New()
if _, err = h.Write(nonce); err != nil {
return
}
if _, err = h.Write([]byte(websocketGUID)); err != nil {
return
}
expected = make([]byte, 28)
base64.StdEncoding.Encode(expected, h.Sum(nil))
return
}
// Client handshake described in draft-ietf-hybi-thewebsocket-protocol-17
func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) {
bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n")
// According to RFC 6874, an HTTP client, proxy, or other
// intermediary must remove any IPv6 zone identifier attached
// to an outgoing URI.
bw.WriteString("Host: " + removeZone(config.Location.Host) + "\r\n")
bw.WriteString("Upgrade: websocket\r\n")
bw.WriteString("Connection: Upgrade\r\n")
nonce := generateNonce()
if config.handshakeData != nil {
nonce = []byte(config.handshakeData["key"])
}
bw.WriteString("Sec-WebSocket-Key: " + string(nonce) + "\r\n")
bw.WriteString("Origin: " + strings.ToLower(config.Origin.String()) + "\r\n")
if config.Version != ProtocolVersionHybi13 {
return ErrBadProtocolVersion
}
bw.WriteString("Sec-WebSocket-Version: " + fmt.Sprintf("%d", config.Version) + "\r\n")
if len(config.Protocol) > 0 {
bw.WriteString("Sec-WebSocket-Protocol: " + strings.Join(config.Protocol, ", ") + "\r\n")
}
// TODO(ukai): send Sec-WebSocket-Extensions.
err = config.Header.WriteSubset(bw, handshakeHeader)
if err != nil {
return err
}
bw.WriteString("\r\n")
if err = bw.Flush(); err != nil {
return err
}
resp, err := http.ReadResponse(br, &http.Request{Method: "GET"})
if err != nil {
return err
}
if resp.StatusCode != 101 {
return ErrBadStatus
}
if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" ||
strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
return ErrBadUpgrade
}
expectedAccept, err := getNonceAccept(nonce)
if err != nil {
return err
}
if resp.Header.Get("Sec-WebSocket-Accept") != string(expectedAccept) {
return ErrChallengeResponse
}
if resp.Header.Get("Sec-WebSocket-Extensions") != "" {
return ErrUnsupportedExtensions
}
offeredProtocol := resp.Header.Get("Sec-WebSocket-Protocol")
if offeredProtocol != "" {
protocolMatched := false
for i := 0; i < len(config.Protocol); i++ {
if config.Protocol[i] == offeredProtocol {
protocolMatched = true
break
}
}
if !protocolMatched {
return ErrBadWebSocketProtocol
}
config.Protocol = []string{offeredProtocol}
}
return nil
}
// newHybiClientConn creates a client WebSocket connection after handshake.
func newHybiClientConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser) *Conn {
return newHybiConn(config, buf, rwc, nil)
}
// A HybiServerHandshaker performs a server handshake using hybi draft protocol.
type hybiServerHandshaker struct {
*Config
accept []byte
}
func (c *hybiServerHandshaker) ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) {
c.Version = ProtocolVersionHybi13
if req.Method != "GET" {
return http.StatusMethodNotAllowed, ErrBadRequestMethod
}
// HTTP version can be safely ignored.
if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" ||
!strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") {
return http.StatusBadRequest, ErrNotWebSocket
}
key := req.Header.Get("Sec-Websocket-Key")
if key == "" {
return http.StatusBadRequest, ErrChallengeResponse
}
version := req.Header.Get("Sec-Websocket-Version")
switch version {
case "13":
c.Version = ProtocolVersionHybi13
default:
return http.StatusBadRequest, ErrBadWebSocketVersion
}
var scheme string
if req.TLS != nil {
scheme = "wss"
} else {
scheme = "ws"
}
c.Location, err = url.ParseRequestURI(scheme + "://" + req.Host + req.URL.RequestURI())
if err != nil {
return http.StatusBadRequest, err
}
protocol := strings.TrimSpace(req.Header.Get("Sec-Websocket-Protocol"))
if protocol != "" {
protocols := strings.Split(protocol, ",")
for i := 0; i < len(protocols); i++ {
c.Protocol = append(c.Protocol, strings.TrimSpace(protocols[i]))
}
}
c.accept, err = getNonceAccept([]byte(key))
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusSwitchingProtocols, nil
}
// Origin parses the Origin header in req.
// If the Origin header is not set, it returns nil and nil.
func Origin(config *Config, req *http.Request) (*url.URL, error) {
var origin string
switch config.Version {
case ProtocolVersionHybi13:
origin = req.Header.Get("Origin")
}
if origin == "" {
return nil, nil
}
return url.ParseRequestURI(origin)
}
func (c *hybiServerHandshaker) AcceptHandshake(buf *bufio.Writer) (err error) {
if len(c.Protocol) > 0 {
if len(c.Protocol) != 1 {
// You need choose a Protocol in Handshake func in Server.
return ErrBadWebSocketProtocol
}
}
buf.WriteString("HTTP/1.1 101 Switching Protocols\r\n")
buf.WriteString("Upgrade: websocket\r\n")
buf.WriteString("Connection: Upgrade\r\n")
buf.WriteString("Sec-WebSocket-Accept: " + string(c.accept) + "\r\n")
if len(c.Protocol) > 0 {
buf.WriteString("Sec-WebSocket-Protocol: " + c.Protocol[0] + "\r\n")
}
// TODO(ukai): send Sec-WebSocket-Extensions.
if c.Header != nil {
err := c.Header.WriteSubset(buf, handshakeHeader)
if err != nil {
return err
}
}
buf.WriteString("\r\n")
return buf.Flush()
}
func (c *hybiServerHandshaker) NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn {
return newHybiServerConn(c.Config, buf, rwc, request)
}
// newHybiServerConn returns a new WebSocket connection speaking hybi draft protocol.
func newHybiServerConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn {
return newHybiConn(config, buf, rwc, request)
}

View File

@ -1,113 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bufio"
"fmt"
"io"
"net/http"
)
func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Request, config *Config, handshake func(*Config, *http.Request) error) (conn *Conn, err error) {
var hs serverHandshaker = &hybiServerHandshaker{Config: config}
code, err := hs.ReadHandshake(buf.Reader, req)
if err == ErrBadWebSocketVersion {
fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\r\n", SupportedProtocolVersion)
buf.WriteString("\r\n")
buf.WriteString(err.Error())
buf.Flush()
return
}
if err != nil {
fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
buf.WriteString("\r\n")
buf.WriteString(err.Error())
buf.Flush()
return
}
if handshake != nil {
err = handshake(config, req)
if err != nil {
code = http.StatusForbidden
fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
buf.WriteString("\r\n")
buf.Flush()
return
}
}
err = hs.AcceptHandshake(buf.Writer)
if err != nil {
code = http.StatusBadRequest
fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
buf.WriteString("\r\n")
buf.Flush()
return
}
conn = hs.NewServerConn(buf, rwc, req)
return
}
// Server represents a server of a WebSocket.
type Server struct {
// Config is a WebSocket configuration for new WebSocket connection.
Config
// Handshake is an optional function in WebSocket handshake.
// For example, you can check, or don't check Origin header.
// Another example, you can select config.Protocol.
Handshake func(*Config, *http.Request) error
// Handler handles a WebSocket connection.
Handler
}
// ServeHTTP implements the http.Handler interface for a WebSocket
func (s Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
s.serveWebSocket(w, req)
}
func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) {
rwc, buf, err := w.(http.Hijacker).Hijack()
if err != nil {
panic("Hijack failed: " + err.Error())
}
// The server should abort the WebSocket connection if it finds
// the client did not send a handshake that matches with protocol
// specification.
defer rwc.Close()
conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake)
if err != nil {
return
}
if conn == nil {
panic("unexpected nil conn")
}
s.Handler(conn)
}
// Handler is a simple interface to a WebSocket browser client.
// It checks if Origin header is valid URL by default.
// You might want to verify websocket.Conn.Config().Origin in the func.
// If you use Server instead of Handler, you could call websocket.Origin and
// check the origin in your Handshake func. So, if you want to accept
// non-browser clients, which do not send an Origin header, set a
// Server.Handshake that does not check the origin.
type Handler func(*Conn)
func checkOrigin(config *Config, req *http.Request) (err error) {
config.Origin, err = Origin(config, req)
if err == nil && config.Origin == nil {
return fmt.Errorf("null origin")
}
return err
}
// ServeHTTP implements the http.Handler interface for a WebSocket
func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
s := Server{Handler: h, Handshake: checkOrigin}
s.serveWebSocket(w, req)
}

View File

@ -1,448 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package websocket implements a client and server for the WebSocket protocol
// as specified in RFC 6455.
//
// This package currently lacks some features found in an alternative
// and more actively maintained WebSocket package:
//
// https://godoc.org/github.com/gorilla/websocket
//
package websocket // import "golang.org/x/net/websocket"
import (
"bufio"
"crypto/tls"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"sync"
"time"
)
const (
ProtocolVersionHybi13 = 13
ProtocolVersionHybi = ProtocolVersionHybi13
SupportedProtocolVersion = "13"
ContinuationFrame = 0
TextFrame = 1
BinaryFrame = 2
CloseFrame = 8
PingFrame = 9
PongFrame = 10
UnknownFrame = 255
DefaultMaxPayloadBytes = 32 << 20 // 32MB
)
// ProtocolError represents WebSocket protocol errors.
type ProtocolError struct {
ErrorString string
}
func (err *ProtocolError) Error() string { return err.ErrorString }
var (
ErrBadProtocolVersion = &ProtocolError{"bad protocol version"}
ErrBadScheme = &ProtocolError{"bad scheme"}
ErrBadStatus = &ProtocolError{"bad status"}
ErrBadUpgrade = &ProtocolError{"missing or bad upgrade"}
ErrBadWebSocketOrigin = &ProtocolError{"missing or bad WebSocket-Origin"}
ErrBadWebSocketLocation = &ProtocolError{"missing or bad WebSocket-Location"}
ErrBadWebSocketProtocol = &ProtocolError{"missing or bad WebSocket-Protocol"}
ErrBadWebSocketVersion = &ProtocolError{"missing or bad WebSocket Version"}
ErrChallengeResponse = &ProtocolError{"mismatch challenge/response"}
ErrBadFrame = &ProtocolError{"bad frame"}
ErrBadFrameBoundary = &ProtocolError{"not on frame boundary"}
ErrNotWebSocket = &ProtocolError{"not websocket protocol"}
ErrBadRequestMethod = &ProtocolError{"bad method"}
ErrNotSupported = &ProtocolError{"not supported"}
)
// ErrFrameTooLarge is returned by Codec's Receive method if payload size
// exceeds limit set by Conn.MaxPayloadBytes
var ErrFrameTooLarge = errors.New("websocket: frame payload size exceeds limit")
// Addr is an implementation of net.Addr for WebSocket.
type Addr struct {
*url.URL
}
// Network returns the network type for a WebSocket, "websocket".
func (addr *Addr) Network() string { return "websocket" }
// Config is a WebSocket configuration
type Config struct {
// A WebSocket server address.
Location *url.URL
// A Websocket client origin.
Origin *url.URL
// WebSocket subprotocols.
Protocol []string
// WebSocket protocol version.
Version int
// TLS config for secure WebSocket (wss).
TlsConfig *tls.Config
// Additional header fields to be sent in WebSocket opening handshake.
Header http.Header
// Dialer used when opening websocket connections.
Dialer *net.Dialer
handshakeData map[string]string
}
// serverHandshaker is an interface to handle WebSocket server side handshake.
type serverHandshaker interface {
// ReadHandshake reads handshake request message from client.
// Returns http response code and error if any.
ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error)
// AcceptHandshake accepts the client handshake request and sends
// handshake response back to client.
AcceptHandshake(buf *bufio.Writer) (err error)
// NewServerConn creates a new WebSocket connection.
NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) (conn *Conn)
}
// frameReader is an interface to read a WebSocket frame.
type frameReader interface {
// Reader is to read payload of the frame.
io.Reader
// PayloadType returns payload type.
PayloadType() byte
// HeaderReader returns a reader to read header of the frame.
HeaderReader() io.Reader
// TrailerReader returns a reader to read trailer of the frame.
// If it returns nil, there is no trailer in the frame.
TrailerReader() io.Reader
// Len returns total length of the frame, including header and trailer.
Len() int
}
// frameReaderFactory is an interface to creates new frame reader.
type frameReaderFactory interface {
NewFrameReader() (r frameReader, err error)
}
// frameWriter is an interface to write a WebSocket frame.
type frameWriter interface {
// Writer is to write payload of the frame.
io.WriteCloser
}
// frameWriterFactory is an interface to create new frame writer.
type frameWriterFactory interface {
NewFrameWriter(payloadType byte) (w frameWriter, err error)
}
type frameHandler interface {
HandleFrame(frame frameReader) (r frameReader, err error)
WriteClose(status int) (err error)
}
// Conn represents a WebSocket connection.
//
// Multiple goroutines may invoke methods on a Conn simultaneously.
type Conn struct {
config *Config
request *http.Request
buf *bufio.ReadWriter
rwc io.ReadWriteCloser
rio sync.Mutex
frameReaderFactory
frameReader
wio sync.Mutex
frameWriterFactory
frameHandler
PayloadType byte
defaultCloseStatus int
// MaxPayloadBytes limits the size of frame payload received over Conn
// by Codec's Receive method. If zero, DefaultMaxPayloadBytes is used.
MaxPayloadBytes int
}
// Read implements the io.Reader interface:
// it reads data of a frame from the WebSocket connection.
// if msg is not large enough for the frame data, it fills the msg and next Read
// will read the rest of the frame data.
// it reads Text frame or Binary frame.
func (ws *Conn) Read(msg []byte) (n int, err error) {
ws.rio.Lock()
defer ws.rio.Unlock()
again:
if ws.frameReader == nil {
frame, err := ws.frameReaderFactory.NewFrameReader()
if err != nil {
return 0, err
}
ws.frameReader, err = ws.frameHandler.HandleFrame(frame)
if err != nil {
return 0, err
}
if ws.frameReader == nil {
goto again
}
}
n, err = ws.frameReader.Read(msg)
if err == io.EOF {
if trailer := ws.frameReader.TrailerReader(); trailer != nil {
io.Copy(ioutil.Discard, trailer)
}
ws.frameReader = nil
goto again
}
return n, err
}
// Write implements the io.Writer interface:
// it writes data as a frame to the WebSocket connection.
func (ws *Conn) Write(msg []byte) (n int, err error) {
ws.wio.Lock()
defer ws.wio.Unlock()
w, err := ws.frameWriterFactory.NewFrameWriter(ws.PayloadType)
if err != nil {
return 0, err
}
n, err = w.Write(msg)
w.Close()
return n, err
}
// Close implements the io.Closer interface.
func (ws *Conn) Close() error {
err := ws.frameHandler.WriteClose(ws.defaultCloseStatus)
err1 := ws.rwc.Close()
if err != nil {
return err
}
return err1
}
func (ws *Conn) IsClientConn() bool { return ws.request == nil }
func (ws *Conn) IsServerConn() bool { return ws.request != nil }
// LocalAddr returns the WebSocket Origin for the connection for client, or
// the WebSocket location for server.
func (ws *Conn) LocalAddr() net.Addr {
if ws.IsClientConn() {
return &Addr{ws.config.Origin}
}
return &Addr{ws.config.Location}
}
// RemoteAddr returns the WebSocket location for the connection for client, or
// the Websocket Origin for server.
func (ws *Conn) RemoteAddr() net.Addr {
if ws.IsClientConn() {
return &Addr{ws.config.Location}
}
return &Addr{ws.config.Origin}
}
var errSetDeadline = errors.New("websocket: cannot set deadline: not using a net.Conn")
// SetDeadline sets the connection's network read & write deadlines.
func (ws *Conn) SetDeadline(t time.Time) error {
if conn, ok := ws.rwc.(net.Conn); ok {
return conn.SetDeadline(t)
}
return errSetDeadline
}
// SetReadDeadline sets the connection's network read deadline.
func (ws *Conn) SetReadDeadline(t time.Time) error {
if conn, ok := ws.rwc.(net.Conn); ok {
return conn.SetReadDeadline(t)
}
return errSetDeadline
}
// SetWriteDeadline sets the connection's network write deadline.
func (ws *Conn) SetWriteDeadline(t time.Time) error {
if conn, ok := ws.rwc.(net.Conn); ok {
return conn.SetWriteDeadline(t)
}
return errSetDeadline
}
// Config returns the WebSocket config.
func (ws *Conn) Config() *Config { return ws.config }
// Request returns the http request upgraded to the WebSocket.
// It is nil for client side.
func (ws *Conn) Request() *http.Request { return ws.request }
// Codec represents a symmetric pair of functions that implement a codec.
type Codec struct {
Marshal func(v interface{}) (data []byte, payloadType byte, err error)
Unmarshal func(data []byte, payloadType byte, v interface{}) (err error)
}
// Send sends v marshaled by cd.Marshal as single frame to ws.
func (cd Codec) Send(ws *Conn, v interface{}) (err error) {
data, payloadType, err := cd.Marshal(v)
if err != nil {
return err
}
ws.wio.Lock()
defer ws.wio.Unlock()
w, err := ws.frameWriterFactory.NewFrameWriter(payloadType)
if err != nil {
return err
}
_, err = w.Write(data)
w.Close()
return err
}
// Receive receives single frame from ws, unmarshaled by cd.Unmarshal and stores
// in v. The whole frame payload is read to an in-memory buffer; max size of
// payload is defined by ws.MaxPayloadBytes. If frame payload size exceeds
// limit, ErrFrameTooLarge is returned; in this case frame is not read off wire
// completely. The next call to Receive would read and discard leftover data of
// previous oversized frame before processing next frame.
func (cd Codec) Receive(ws *Conn, v interface{}) (err error) {
ws.rio.Lock()
defer ws.rio.Unlock()
if ws.frameReader != nil {
_, err = io.Copy(ioutil.Discard, ws.frameReader)
if err != nil {
return err
}
ws.frameReader = nil
}
again:
frame, err := ws.frameReaderFactory.NewFrameReader()
if err != nil {
return err
}
frame, err = ws.frameHandler.HandleFrame(frame)
if err != nil {
return err
}
if frame == nil {
goto again
}
maxPayloadBytes := ws.MaxPayloadBytes
if maxPayloadBytes == 0 {
maxPayloadBytes = DefaultMaxPayloadBytes
}
if hf, ok := frame.(*hybiFrameReader); ok && hf.header.Length > int64(maxPayloadBytes) {
// payload size exceeds limit, no need to call Unmarshal
//
// set frameReader to current oversized frame so that
// the next call to this function can drain leftover
// data before processing the next frame
ws.frameReader = frame
return ErrFrameTooLarge
}
payloadType := frame.PayloadType()
data, err := ioutil.ReadAll(frame)
if err != nil {
return err
}
return cd.Unmarshal(data, payloadType, v)
}
func marshal(v interface{}) (msg []byte, payloadType byte, err error) {
switch data := v.(type) {
case string:
return []byte(data), TextFrame, nil
case []byte:
return data, BinaryFrame, nil
}
return nil, UnknownFrame, ErrNotSupported
}
func unmarshal(msg []byte, payloadType byte, v interface{}) (err error) {
switch data := v.(type) {
case *string:
*data = string(msg)
return nil
case *[]byte:
*data = msg
return nil
}
return ErrNotSupported
}
/*
Message is a codec to send/receive text/binary data in a frame on WebSocket connection.
To send/receive text frame, use string type.
To send/receive binary frame, use []byte type.
Trivial usage:
import "websocket"
// receive text frame
var message string
websocket.Message.Receive(ws, &message)
// send text frame
message = "hello"
websocket.Message.Send(ws, message)
// receive binary frame
var data []byte
websocket.Message.Receive(ws, &data)
// send binary frame
data = []byte{0, 1, 2}
websocket.Message.Send(ws, data)
*/
var Message = Codec{marshal, unmarshal}
func jsonMarshal(v interface{}) (msg []byte, payloadType byte, err error) {
msg, err = json.Marshal(v)
return msg, TextFrame, err
}
func jsonUnmarshal(msg []byte, payloadType byte, v interface{}) (err error) {
return json.Unmarshal(msg, v)
}
/*
JSON is a codec to send/receive JSON data in a frame from a WebSocket connection.
Trivial usage:
import "websocket"
type T struct {
Msg string
Count int
}
// receive JSON type T
var data T
websocket.JSON.Receive(ws, &data)
// send JSON type T
websocket.JSON.Send(ws, data)
*/
var JSON = Codec{jsonMarshal, jsonUnmarshal}

6
vendor/modules.txt vendored
View File

@ -95,7 +95,7 @@ github.com/nicksnyder/go-i18n/i18n
github.com/nicksnyder/go-i18n/i18n/bundle github.com/nicksnyder/go-i18n/i18n/bundle
github.com/nicksnyder/go-i18n/i18n/language github.com/nicksnyder/go-i18n/i18n/language
github.com/nicksnyder/go-i18n/i18n/translation github.com/nicksnyder/go-i18n/i18n/translation
# github.com/nlopes/slack v0.0.0-20180101221843-107290b5bbaf # github.com/nlopes/slack v0.3.1-0.20180805133408-21749ab136a8
github.com/nlopes/slack github.com/nlopes/slack
# github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83 # github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83
github.com/paulrosania/go-charset/charset github.com/paulrosania/go-charset/charset
@ -106,6 +106,8 @@ github.com/pborman/uuid
github.com/pelletier/go-toml github.com/pelletier/go-toml
# github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271 # github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271
github.com/peterhellberg/emojilib github.com/peterhellberg/emojilib
# github.com/pkg/errors v0.8.0
github.com/pkg/errors
# github.com/pmezard/go-difflib v1.0.0 # github.com/pmezard/go-difflib v1.0.0
github.com/pmezard/go-difflib/difflib github.com/pmezard/go-difflib/difflib
# github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a # github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a
@ -155,8 +157,6 @@ golang.org/x/crypto/ed25519
golang.org/x/crypto/internal/chacha20 golang.org/x/crypto/internal/chacha20
golang.org/x/crypto/blowfish golang.org/x/crypto/blowfish
golang.org/x/crypto/ed25519/internal/edwards25519 golang.org/x/crypto/ed25519/internal/edwards25519
# golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37
golang.org/x/net/websocket
# golang.org/x/sys v0.0.0-20171130163741-8b4580aae2a0 # golang.org/x/sys v0.0.0-20171130163741-8b4580aae2a0
golang.org/x/sys/unix golang.org/x/sys/unix
golang.org/x/sys/windows golang.org/x/sys/windows