diff options
Diffstat (limited to 'vendor/github.com')
45 files changed, 2627 insertions, 696 deletions
diff --git a/vendor/github.com/nlopes/slack/.gitignore b/vendor/github.com/nlopes/slack/.gitignore index dd2440d5..ac6f3eeb 100644 --- a/vendor/github.com/nlopes/slack/.gitignore +++ b/vendor/github.com/nlopes/slack/.gitignore @@ -1,2 +1,3 @@ *.test *~ +.idea/ diff --git a/vendor/github.com/nlopes/slack/CHANGELOG.md b/vendor/github.com/nlopes/slack/CHANGELOG.md index 8c4772da..a79ea50c 100644 --- a/vendor/github.com/nlopes/slack/CHANGELOG.md +++ b/vendor/github.com/nlopes/slack/CHANGELOG.md @@ -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
This is released before adding context support.
diff --git a/vendor/github.com/nlopes/slack/Gopkg.lock b/vendor/github.com/nlopes/slack/Gopkg.lock new file mode 100644 index 00000000..5cc0520e --- /dev/null +++ b/vendor/github.com/nlopes/slack/Gopkg.lock @@ -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 diff --git a/vendor/github.com/nlopes/slack/README.md b/vendor/github.com/nlopes/slack/README.md index 953b9d8f..849e8bdc 100644 --- a/vendor/github.com/nlopes/slack/README.md +++ b/vendor/github.com/nlopes/slack/README.md @@ -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 a fully managed way. + + ## Change log +Support for the EventsAPI has recently been added. It is still in its early stages but nearly all events have been added and tested (except for those events in [Developer Preview](https://api.slack.com/slack-apps-preview) mode). API stability for events is not promised at this time. -### v0.1.0 - May 28, 2017 +### v0.2.0 - Feb 10, 2018 -This is released before adding context support. -As the used context package is the one from Go 1.7 this will be the last -compatible with Go < 1.7. +Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against. -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 -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 @@ -79,6 +80,11 @@ func main() { 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 You are more than welcome to contribute to this project. Fork and diff --git a/vendor/github.com/nlopes/slack/admin.go b/vendor/github.com/nlopes/slack/admin.go index 478c4f40..a2aa7e5c 100644 --- a/vendor/github.com/nlopes/slack/admin.go +++ b/vendor/github.com/nlopes/slack/admin.go @@ -12,9 +12,9 @@ type adminResponse struct { 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{} - err := parseAdminResponse(ctx, method, teamName, values, adminResponse, debug) + err := parseAdminResponse(ctx, client, method, teamName, values, adminResponse, debug) if err != nil { 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 { values := url.Values{ "user": {uid}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "setInactive", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "setInactive", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err) } @@ -61,12 +61,13 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi "first_name": {firstName}, "last_name": {lastName}, "ultra_restricted": {"1"}, - "token": {api.config.token}, + "token": {api.token}, + "resend": {"true"}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "invite", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) if err != nil { 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}, "last_name": {lastName}, "restricted": {"1"}, - "token": {api.config.token}, + "token": {api.token}, + "resend": {"true"}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "invite", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to restricted account: %s", err) } @@ -111,12 +113,12 @@ func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName, "email": {emailAddress}, "first_name": {firstName}, "last_name": {lastName}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "invite", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) if err != nil { 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 { values := url.Values{ "user": {user}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "setRegular", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "setRegular", teamName, values, api.debug) if err != nil { 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 { values := url.Values{ "user": {user}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "sendSSOBind", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "sendSSOBind", teamName, values, api.debug) if err != nil { 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{ "user": {uid}, "channel": {channel}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "setUltraRestricted", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "setUltraRestricted", teamName, values, api.debug) if err != nil { 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 { values := url.Values{ "user": {uid}, - "token": {api.config.token}, + "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } - _, err := adminRequest(ctx, "setRestricted", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "setRestricted", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to restrict account: %s", err) } diff --git a/vendor/github.com/nlopes/slack/attachments.go b/vendor/github.com/nlopes/slack/attachments.go index abc94e73..326fc010 100644 --- a/vendor/github.com/nlopes/slack/attachments.go +++ b/vendor/github.com/nlopes/slack/attachments.go @@ -59,6 +59,7 @@ type AttachmentActionCallback struct { AttachmentID string `json:"attachment_id"` Token string `json:"token"` ResponseURL string `json:"response_url"` + TriggerID string `json:"trigger_id"` } // ConfirmationField are used to ask users to confirm actions @@ -75,7 +76,9 @@ type Attachment struct { Fallback string `json:"fallback"` CallbackID string `json:"callback_id,omitempty"` + ID int `json:"id,omitempty"` + AuthorID string `json:"author_id,omitempty"` AuthorName string `json:"author_name,omitempty"` AuthorSubname string `json:"author_subname,omitempty"` AuthorLink string `json:"author_link,omitempty"` diff --git a/vendor/github.com/nlopes/slack/backoff.go b/vendor/github.com/nlopes/slack/backoff.go index e555a1ad..197bce2e 100644 --- a/vendor/github.com/nlopes/slack/backoff.go +++ b/vendor/github.com/nlopes/slack/backoff.go @@ -38,7 +38,7 @@ func (b *backoff) Duration() time.Duration { } //calculate this duration 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) } //cap! diff --git a/vendor/github.com/nlopes/slack/bots.go b/vendor/github.com/nlopes/slack/bots.go index 13a78cb1..92570a04 100644 --- a/vendor/github.com/nlopes/slack/bots.go +++ b/vendor/github.com/nlopes/slack/bots.go @@ -19,9 +19,9 @@ type botResponseFull struct { 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{} - err := post(ctx, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { 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 func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { return nil, err } diff --git a/vendor/github.com/nlopes/slack/channels.go b/vendor/github.com/nlopes/slack/channels.go index 9490bc70..6204315a 100644 --- a/vendor/github.com/nlopes/slack/channels.go +++ b/vendor/github.com/nlopes/slack/channels.go @@ -20,14 +20,15 @@ type channelResponseFull struct { // Channel contains information about the channel type Channel struct { groupConversation - IsChannel bool `json:"is_channel"` - IsGeneral bool `json:"is_general"` - IsMember bool `json:"is_member"` + IsChannel bool `json:"is_channel"` + IsGeneral bool `json:"is_general"` + 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{} - err := post(ctx, path, values, response, debug) + err := postForm(ctx, client, SLACK_API+path, values, response, debug) if err != nil { return nil, err } @@ -45,12 +46,13 @@ func (api *Client) ArchiveChannel(channelID string) error { // ArchiveChannelContext archives the given channel with a custom context // 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{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channelID}, } - _, err := channelRequest(ctx, "channels.archive", values, api.debug) + + _, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug) return err } @@ -62,12 +64,13 @@ func (api *Client) UnarchiveChannel(channelID string) error { // UnarchiveChannelContext unarchives the given channel with a custom context // 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{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channelID}, } - _, err := channelRequest(ctx, "channels.unarchive", values, api.debug) + + _, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug) return err } @@ -81,10 +84,11 @@ func (api *Client) CreateChannel(channelName string) (*Channel, error) { // see https://api.slack.com/methods/channels.create func (api *Client) CreateChannelContext(ctx context.Context, channelName string) (*Channel, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { return nil, err } @@ -101,7 +105,7 @@ func (api *Client) GetChannelHistory(channelID string, params HistoryParameters) // see https://api.slack.com/methods/channels.history func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID string, params HistoryParameters) (*History, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channelID}, } if params.Latest != DEFAULT_HISTORY_LATEST { @@ -120,6 +124,7 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID strin values.Add("inclusive", "0") } } + if params.Unreads != DEFAULT_HISTORY_UNREADS { if params.Unreads { values.Add("unreads", "1") @@ -127,7 +132,8 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID strin 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 { return nil, err } @@ -144,10 +150,11 @@ func (api *Client) GetChannelInfo(channelID string) (*Channel, error) { // see https://api.slack.com/methods/channels.info func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { return nil, err } @@ -164,11 +171,12 @@ func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error) // see https://api.slack.com/methods/channels.invite func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channelID}, "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 { return nil, err } @@ -185,10 +193,11 @@ func (api *Client) JoinChannel(channelName string) (*Channel, error) { // see https://api.slack.com/methods/channels.join func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (*Channel, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { return nil, err } @@ -205,17 +214,16 @@ func (api *Client) LeaveChannel(channelID string) (bool, error) { // see https://api.slack.com/methods/channels.leave func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (bool, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { return false, err } - if response.NotInChannel { - return response.NotInChannel, nil - } - return false, nil + + return response.NotInChannel, nil } // 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 // 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{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channelID}, "user": {user}, } - _, err := channelRequest(ctx, "channels.kick", values, api.debug) + + _, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug) return err } @@ -246,12 +255,13 @@ func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) { // see https://api.slack.com/methods/channels.list func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) ([]Channel, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if excludeArchived { 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 { 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 // For more details see SetChannelReadMark documentation // 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{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channelID}, "ts": {ts}, } - _, err := channelRequest(ctx, "channels.mark", values, api.debug) + + _, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug) return err } @@ -291,13 +302,14 @@ func (api *Client) RenameChannel(channelID, name string) (*Channel, error) { // see https://api.slack.com/methods/channels.rename func (api *Client) RenameChannelContext(ctx context.Context, channelID, name string) (*Channel, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channelID}, "name": {name}, } + // XXX: the created entry in this call returns a string instead of a number // so I may have to do some workaround to solve it. - response, err := channelRequest(ctx, "channels.rename", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.rename", values, api.debug) if err != nil { return nil, err } @@ -314,11 +326,12 @@ func (api *Client) SetChannelPurpose(channelID, purpose string) (string, error) // see https://api.slack.com/methods/channels.setPurpose func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purpose string) (string, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channelID}, "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 { return "", err } @@ -335,11 +348,12 @@ func (api *Client) SetChannelTopic(channelID, topic string) (string, error) { // see https://api.slack.com/methods/channels.setTopic func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic string) (string, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channelID}, "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 { return "", err } @@ -356,11 +370,11 @@ func (api *Client) GetChannelReplies(channelID, thread_ts string) ([]Message, er // see https://api.slack.com/methods/channels.replies func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channelID}, "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 { return nil, err } diff --git a/vendor/github.com/nlopes/slack/chat.go b/vendor/github.com/nlopes/slack/chat.go index 0eb042df..2b89a44c 100644 --- a/vendor/github.com/nlopes/slack/chat.go +++ b/vendor/github.com/nlopes/slack/chat.go @@ -3,17 +3,16 @@ package slack import ( "context" "encoding/json" - "errors" "net/url" "strings" ) const ( DEFAULT_MESSAGE_USERNAME = "" - DEFAULT_MESSAGE_THREAD_TIMESTAMP = "" DEFAULT_MESSAGE_REPLY_BROADCAST = false DEFAULT_MESSAGE_ASUSER = false DEFAULT_MESSAGE_PARSE = "" + DEFAULT_MESSAGE_THREAD_TIMESTAMP = "" DEFAULT_MESSAGE_LINK_NAMES = 0 DEFAULT_MESSAGE_UNFURL_LINKS = false DEFAULT_MESSAGE_UNFURL_MEDIA = true @@ -24,16 +23,26 @@ const ( ) type chatResponseFull struct { - Channel string `json:"channel"` - Timestamp string `json:"ts"` - Text string `json:"text"` + Channel string `json:"channel"` + Timestamp string `json:"ts"` //Regualr message timestamp + MessageTimeStamp string `json:"message_ts"` //Ephemeral message timestamp + Text string `json:"text"` 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 type PostMessageParameters struct { - Text string `json:"text"` - Username string `json:"user_name"` + Username string `json:"username"` AsUser bool `json:"as_user"` Parse string `json:"parse"` 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 func NewPostMessageParameters() PostMessageParameters { return PostMessageParameters{ - Username: DEFAULT_MESSAGE_USERNAME, - User: DEFAULT_MESSAGE_USERNAME, - AsUser: DEFAULT_MESSAGE_ASUSER, - Parse: DEFAULT_MESSAGE_PARSE, - LinkNames: DEFAULT_MESSAGE_LINK_NAMES, - Attachments: nil, - UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS, - UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA, - IconURL: DEFAULT_MESSAGE_ICON_URL, - IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI, - Markdown: DEFAULT_MESSAGE_MARKDOWN, - EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT, + Username: DEFAULT_MESSAGE_USERNAME, + User: DEFAULT_MESSAGE_USERNAME, + AsUser: DEFAULT_MESSAGE_ASUSER, + Parse: DEFAULT_MESSAGE_PARSE, + ThreadTimestamp: DEFAULT_MESSAGE_THREAD_TIMESTAMP, + LinkNames: DEFAULT_MESSAGE_LINK_NAMES, + Attachments: nil, + UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS, + UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA, + IconURL: DEFAULT_MESSAGE_ICON_URL, + IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI, + 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. // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. -func (api *Client) PostEphemeral(channel, userID string, options ...MsgOption) (string, error) { - options = append(options, MsgOptionPostEphemeral()) +func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) (string, error) { return api.PostEphemeralContext( context.Background(), - channel, + channelID, userID, 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 // For more details, see PostEphemeral documentation -func (api *Client) PostEphemeralContext(ctx context.Context, channel, userID string, options ...MsgOption) (string, error) { - path, values, err := ApplyMsgOptions(api.config.token, channel, options...) - if err != nil { - return "", err - } - - values.Add("user", userID) - - response, err := chatRequest(ctx, path, values, api.debug) - if err != nil { - return "", err - } - - return response.Timestamp, nil +func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID string, options ...MsgOption) (timestamp string, err error) { + _, timestamp, _, err = api.SendMessageContext(ctx, channelID, append(options, MsgOptionPostEphemeral2(userID))...) + return timestamp, err } // UpdateMessage updates a message in a channel -func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) { - return api.UpdateMessageContext(context.Background(), channel, timestamp, text) +func (api *Client) UpdateMessage(channelID, timestamp, text string) (string, string, string, error) { + return api.UpdateMessageContext(context.Background(), channelID, timestamp, text) } -// UpdateMessage updates a message in a channel -func (api *Client) UpdateMessageContext(ctx context.Context, channel, timestamp, text string) (string, string, string, error) { - return api.SendMessageContext(ctx, channel, MsgOptionUpdate(timestamp), MsgOptionText(text, true)) +// UpdateMessageContext updates a message in a channel +func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp, text string) (string, string, string, error) { + return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionText(text, true)) } // 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. -func (api *Client) SendMessageContext(ctx context.Context, channel string, options ...MsgOption) (string, string, string, error) { - channel, values, err := ApplyMsgOptions(api.config.token, channel, options...) - if err != nil { +func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (channel string, timestamp string, text string, err error) { + var ( + config sendConfig + response chatResponseFull + ) + + if config, err = applyMsgOptions(api.token, channelID, options...); err != nil { return "", "", "", err } - response, err := chatRequest(ctx, channel, values, api.debug) - if err != nil { + if err = postSlackMethod(ctx, api.httpclient, string(config.mode), config.values, &response, api.debug); err != nil { 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. 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{ mode: chatPostMessage, values: url.Values{ @@ -182,11 +188,11 @@ func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.V for _, opt := range options { 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 { @@ -194,18 +200,6 @@ func escapeMessage(message string) string { 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 const ( @@ -213,6 +207,7 @@ const ( chatPostMessage sendMode = "chat.postMessage" chatDelete sendMode = "chat.delete" chatPostEphemeral sendMode = "chat.postEphemeral" + chatMeMessage sendMode = "chat.meMessage" ) 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 { return func(config *sendConfig) error { 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. func MsgOptionUpdate(timestamp string) MsgOption { 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 // text. 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. func MsgOptionDisableMediaUnfurl() MsgOption { 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. func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { return func(config *sendConfig) error { if params.Username != DEFAULT_MESSAGE_USERNAME { - config.values.Set("username", string(params.Username)) + config.values.Set("username", params.Username) } // chat.postEphemeral support @@ -336,7 +408,7 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { MsgOptionAsUser(params.AsUser)(config) 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 { config.values.Set("link_names", "1") diff --git a/vendor/github.com/nlopes/slack/conversation.go b/vendor/github.com/nlopes/slack/conversation.go index 83a1d4ee..edde87a2 100644 --- a/vendor/github.com/nlopes/slack/conversation.go +++ b/vendor/github.com/nlopes/slack/conversation.go @@ -1,5 +1,13 @@ package slack +import ( + "context" + "errors" + "net/url" + "strconv" + "strings" +) + // Conversation is the foundation for IM and BaseGroupConversation type conversation struct { ID string `json:"id"` @@ -9,6 +17,20 @@ type conversation struct { Latest *Message `json:"latest,omitempty"` UnreadCount int `json:"unread_count,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 @@ -35,3 +57,510 @@ type Purpose struct { Creator string `json:"creator"` 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 +} diff --git a/vendor/github.com/nlopes/slack/dialog.go b/vendor/github.com/nlopes/slack/dialog.go new file mode 100644 index 00000000..a13e53da --- /dev/null +++ b/vendor/github.com/nlopes/slack/dialog.go @@ -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() +} diff --git a/vendor/github.com/nlopes/slack/dnd.go b/vendor/github.com/nlopes/slack/dnd.go index 4f1b3228..26d36d6a 100644 --- a/vendor/github.com/nlopes/slack/dnd.go +++ b/vendor/github.com/nlopes/slack/dnd.go @@ -36,9 +36,9 @@ type dndTeamInfoResponse struct { 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{} - err := post(ctx, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { 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 func (api *Client) EndDNDContext(ctx context.Context) error { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } 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 } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // 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 func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { 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 { 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. func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDStatus, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if user != nil { 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 { 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. func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (map[string]DNDStatus, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "users": {strings.Join(users, ",")}, } 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 } if !response.Ok { @@ -139,10 +140,11 @@ func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) { // For more information see the SetSnooze docs func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { return nil, err } diff --git a/vendor/github.com/nlopes/slack/emoji.go b/vendor/github.com/nlopes/slack/emoji.go index 5da9da41..fe2945c4 100644 --- a/vendor/github.com/nlopes/slack/emoji.go +++ b/vendor/github.com/nlopes/slack/emoji.go @@ -19,10 +19,11 @@ func (api *Client) GetEmoji() (map[string]string, error) { // GetEmojiContext retrieves all the emojis with a custom context func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } 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 { return nil, err } diff --git a/vendor/github.com/nlopes/slack/files.go b/vendor/github.com/nlopes/slack/files.go index 68941422..2381ec3c 100644 --- a/vendor/github.com/nlopes/slack/files.go +++ b/vendor/github.com/nlopes/slack/files.go @@ -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{} - err := post(ctx, path, values, response, debug) + err := postForm(ctx, client, SLACK_API+path, values, response, debug) if err != nil { 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 func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*File, []Comment, *Paging, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "file": {fileID}, "count": {strconv.Itoa(count)}, "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 { 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 func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if params.User != DEFAULT_FILES_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 { 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 { return nil, nil, err } @@ -221,7 +223,7 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam } response := &fileResponseFull{} values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if params.Filetype != "" { values.Add("filetype", params.Filetype) @@ -240,11 +242,11 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam } if 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 != "" { - 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 { - 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 { return nil, err @@ -255,20 +257,40 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam 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 func (api *Client) DeleteFile(fileID string) error { return api.DeleteFileContext(context.Background(), fileID) } // 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{ - "token": {api.config.token}, + "token": {api.token}, "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 @@ -279,10 +301,11 @@ func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) { // RevokeFilePublicURLContext disables public/external sharing for a file with a custom context func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string) (*File, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { 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 func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) (*File, []Comment, *Paging, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { return nil, nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/groups.go b/vendor/github.com/nlopes/slack/groups.go index d7f39144..67e78e99 100644 --- a/vendor/github.com/nlopes/slack/groups.go +++ b/vendor/github.com/nlopes/slack/groups.go @@ -28,9 +28,9 @@ type groupResponseFull struct { 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{} - err := post(ctx, path, values, response, debug) + err := postForm(ctx, client, SLACK_API+path, values, response, debug) if err != nil { return nil, err } @@ -45,17 +45,15 @@ func (api *Client) ArchiveGroup(group string) error { 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 { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, } - _, err := groupRequest(ctx, "groups.archive", values, api.debug) - if err != nil { - return err - } - return nil + + _, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api.debug) + return err } // UnarchiveGroup unarchives a private group @@ -63,17 +61,15 @@ func (api *Client) UnarchiveGroup(group string) error { 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 { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, } - _, err := groupRequest(ctx, "groups.unarchive", values, api.debug) - if err != nil { - return err - } - return nil + + _, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api.debug) + return err } // CreateGroup creates a private group @@ -81,13 +77,14 @@ func (api *Client) CreateGroup(group string) (*Group, error) { 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) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { return nil, err } @@ -104,14 +101,15 @@ func (api *Client) CreateChildGroup(group string) (*Group, error) { 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 func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (*Group, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { 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 func (api *Client) CloseGroupContext(ctx context.Context, group string) (bool, bool, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { 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 func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, params HistoryParameters) (*History, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, } if params.Latest != DEFAULT_HISTORY_LATEST { @@ -170,7 +169,8 @@ func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, par 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 { 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 func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user string) (*Group, bool, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, "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 { 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 -func (api *Client) LeaveGroupContext(ctx context.Context, group string) error { +func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, } - _, err := groupRequest(ctx, "groups.leave", values, api.debug) + + _, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug) 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 -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{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, "user": {user}, } - _, err := groupRequest(ctx, "groups.kick", values, api.debug) + + _, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug) return err } @@ -235,12 +238,13 @@ func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) { // GetGroupsContext retrieves all groups with a custom context func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) ([]Group, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if excludeArchived { 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 { return nil, err } @@ -255,10 +259,11 @@ func (api *Client) GetGroupInfo(group string) (*Group, error) { // GetGroupInfoContext retrieves the given group with a custom context func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { 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 // 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{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, "ts": {ts}, } - _, err := groupRequest(ctx, "groups.mark", values, api.debug) + + _, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug) return err } @@ -294,10 +300,11 @@ func (api *Client) OpenGroup(group string) (bool, bool, error) { // OpenGroupContext opens a private group with a custom context func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bool, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { 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 func (api *Client) RenameGroupContext(ctx context.Context, group, name string) (*Channel, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, "name": {name}, } + // XXX: the created entry in this call returns a string instead of a number // so I may have to do some workaround to solve it. - response, err := groupRequest(ctx, "groups.rename", values, api.debug) + response, err := groupRequest(ctx, api.httpclient, "groups.rename", values, api.debug) if err != nil { 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 func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose string) (string, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, "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 { return "", err } @@ -354,11 +363,12 @@ func (api *Client) SetGroupTopic(group, topic string) (string, error) { // SetGroupTopicContext sets the group topic with a custom context func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string) (string, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {group}, "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 { return "", err } diff --git a/vendor/github.com/nlopes/slack/im.go b/vendor/github.com/nlopes/slack/im.go index 0cbc8d34..fa8b0959 100644 --- a/vendor/github.com/nlopes/slack/im.go +++ b/vendor/github.com/nlopes/slack/im.go @@ -29,9 +29,9 @@ type IM struct { 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{} - err := post(ctx, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { 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 func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (bool, bool, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { 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 func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool, bool, string, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { 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 -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{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channel}, "ts": {ts}, } - _, err = imRequest(ctx, "im.mark", values, api.debug) - if err != nil { - return err - } - return + + _, err := imRequest(ctx, api.httpclient, "im.mark", values, api.debug) + return err } // 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 func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "channel": {channel}, } if params.Latest != DEFAULT_HISTORY_LATEST { @@ -132,7 +132,8 @@ func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, para 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 { 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 func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) { 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 { return nil, err } diff --git a/vendor/github.com/nlopes/slack/info.go b/vendor/github.com/nlopes/slack/info.go index 49db5327..db8534c7 100644 --- a/vendor/github.com/nlopes/slack/info.go +++ b/vendor/github.com/nlopes/slack/info.go @@ -1,7 +1,9 @@ package slack import ( + "bytes" "fmt" + "strconv" "time" ) @@ -127,6 +129,19 @@ func (t JSONTime) Time() time.Time { 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 type Team struct { ID string `json:"id"` @@ -156,7 +171,7 @@ type Info struct { type infoResponseFull struct { Info - WebResponse + SlackResponse } // GetBotByID returns a bot given a bot id diff --git a/vendor/github.com/nlopes/slack/logger.go b/vendor/github.com/nlopes/slack/logger.go new file mode 100644 index 00000000..501d1672 --- /dev/null +++ b/vendor/github.com/nlopes/slack/logger.go @@ -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...)) +} diff --git a/vendor/github.com/nlopes/slack/messages.go b/vendor/github.com/nlopes/slack/messages.go index cdb13098..6551dd4f 100644 --- a/vendor/github.com/nlopes/slack/messages.go +++ b/vendor/github.com/nlopes/slack/messages.go @@ -2,12 +2,13 @@ package slack // OutgoingMessage is used for the realtime API, and seems incomplete. type OutgoingMessage struct { - ID int `json:"id"` + ID int `json:"id"` // channel ID Channel string `json:"channel,omitempty"` Text string `json:"text,omitempty"` Type string `json:"type,omitempty"` ThreadTimestamp string `json:"thread_ts,omitempty"` + ThreadBroadcast bool `json:"reply_broadcast,omitempty"` } // 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"` ThreadTimestamp string `json:"thread_ts,omitempty"` IsStarred bool `json:"is_starred,omitempty"` - PinnedTo []string `json:"pinned_to, omitempty"` + PinnedTo []string `json:"pinned_to,omitempty"` Attachments []Attachment `json:"attachments,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 SubType string `json:"subtype,omitempty"` @@ -65,7 +69,7 @@ type Msg struct { ParentUserId string `json:"parent_user_id,omitempty"` // file_share, file_comment, file_mention - File *File `json:"file,omitempty"` + Files []File `json:"files,omitempty"` // file_share Upload bool `json:"upload,omitempty"` @@ -82,6 +86,11 @@ type Msg struct { // reactions 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 @@ -109,27 +118,33 @@ type Event struct { // Ping contains information about a Ping Event type Ping struct { - ID int `json:"id"` - Type string `json:"type"` + ID int `json:"id"` + Type string `json:"type"` + Timestamp int64 `json:"timestamp"` } // Pong contains information about a Pong Event type Pong struct { - Type string `json:"type"` - ReplyTo int `json:"reply_to"` + Type string `json:"type"` + ReplyTo int `json:"reply_to"` + Timestamp int64 `json:"timestamp"` } // NewOutgoingMessage prepares an OutgoingMessage that the user can // use to send a message. Use this function to properly set the // 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() - return &OutgoingMessage{ + msg := OutgoingMessage{ ID: id, Type: "message", Channel: channelID, Text: text, } + for _, option := range options { + option(&msg) + } + return &msg } // NewTypingMessage prepares an OutgoingMessage that the user can @@ -143,3 +158,21 @@ func (rtm *RTM) NewTypingMessage(channelID string) *OutgoingMessage { 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 + } + +} diff --git a/vendor/github.com/nlopes/slack/misc.go b/vendor/github.com/nlopes/slack/misc.go index e890aa8f..69103384 100644 --- a/vendor/github.com/nlopes/slack/misc.go +++ b/vendor/github.com/nlopes/slack/misc.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -18,29 +19,41 @@ import ( "time" ) -// 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) +type SlackResponse struct { + Ok bool `json:"ok"` + Error string `json:"error"` } -var customHTTPClient HTTPRequester +func (t SlackResponse) Err() error { + if t.Ok { + return nil + } + + // handle pure text based responses like chat.post + // which while they have a slack response in their data structure + // it doesn't actually get set during parsing. + if strings.TrimSpace(t.Error) == "" { + return nil + } -// HTTPClient sets a custom http.Client -// deprecated: in favor of SetHTTPClient() -var HTTPClient = &http.Client{} + return errors.New(t.Error) +} -type WebResponse struct { - Ok bool `json:"ok"` - Error *WebError `json:"error"` +// StatusCodeError represents an http response error. +// type httpStatusCode interface { HTTPStatusCode() int } to handle it. +type statusCodeError struct { + Code int + Status string } -type WebError string +func (t statusCodeError) Error() string { + // TODO: this is a bad error string, should clean it up with a breaking changes + // merger. + return fmt.Sprintf("Slack server error: %s.", t.Status) +} -func (s WebError) Error() string { - return string(s) +func (t statusCodeError) HTTPStatusCode() int { + return t.Code } type RateLimitedError struct { @@ -77,7 +90,7 @@ func fileUploadReq(ctx context.Context, path, fieldname, filename string, values 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) if err != nil { return err @@ -88,10 +101,10 @@ func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error 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) if err != nil { return err @@ -101,16 +114,16 @@ func postLocalWithMultipartResponse(ctx context.Context, path, fpath, fieldname return err } 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) if err != nil { return err } req = req.WithContext(ctx) - resp, err := getHTTPClient().Do(req) + resp, err := client.Do(req) if err != nil { return err } @@ -127,22 +140,15 @@ 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. if resp.StatusCode != http.StatusOK { 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 { - reqBody := strings.NewReader(values.Encode()) - req, err := http.NewRequest("POST", endpoint, reqBody) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - +func doPost(ctx context.Context, client HTTPRequester, req *http.Request, intf interface{}, debug bool) error { req = req.WithContext(ctx) - resp, err := getHTTPClient().Do(req) + resp, err := client.Do(req) if err != nil { return err } @@ -159,19 +165,43 @@ func postForm(ctx context.Context, endpoint string, values url.Values, intf inte // 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 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) +} - return parseResponseBody(resp.Body, &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()) + req, err := http.NewRequest("POST", endpoint, reqBody) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return doPost(ctx, client, req, intf, debug) } -func post(ctx context.Context, path string, values url.Values, intf interface{}, debug bool) error { - return postForm(ctx, SLACK_API+path, values, intf, debug) +// post to a slack web method. +func postSlackMethod(ctx context.Context, client HTTPRequester, path string, values url.Values, intf interface{}, debug bool) error { + return postForm(ctx, client, SLACK_API+path, values, intf, debug) } -func 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()) - return postForm(ctx, endpoint, values, intf, debug) + return postForm(ctx, client, endpoint, values, intf, debug) } func logResponse(resp *http.Response, debug bool) error { @@ -187,17 +217,24 @@ func logResponse(resp *http.Response, debug bool) error { return nil } -func getHTTPClient() HTTPRequester { - if customHTTPClient != nil { - return customHTTPClient - } +func okJSONHandler(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + response, _ := json.Marshal(SlackResponse{ + Ok: true, + }) + rw.Write(response) +} + +type errorString string - return HTTPClient +func (t errorString) Error() string { + return string(t) } -// 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 +// 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) } diff --git a/vendor/github.com/nlopes/slack/oauth.go b/vendor/github.com/nlopes/slack/oauth.go index db10aa1b..378af4a5 100644 --- a/vendor/github.com/nlopes/slack/oauth.go +++ b/vendor/github.com/nlopes/slack/oauth.go @@ -55,7 +55,7 @@ func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code, "redirect_uri": {redirectURI}, } response := &OAuthResponse{} - err = post(ctx, "oauth.access", values, response, debug) + err = postSlackMethod(ctx, customHTTPClient, "oauth.access", values, response, debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/pins.go b/vendor/github.com/nlopes/slack/pins.go index a20f8f73..34863f17 100644 --- a/vendor/github.com/nlopes/slack/pins.go +++ b/vendor/github.com/nlopes/slack/pins.go @@ -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 { values := url.Values{ "channel": {channel}, - "token": {api.config.token}, + "token": {api.token}, } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } + 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 } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // 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 { values := url.Values{ "channel": {channel}, - "token": {api.config.token}, + "token": {api.token}, } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } + 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 } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // 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) { values := url.Values{ "channel": {channel}, - "token": {api.config.token}, + "token": {api.token}, } + 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 { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/reactions.go b/vendor/github.com/nlopes/slack/reactions.go index 9da59241..5eabde63 100644 --- a/vendor/github.com/nlopes/slack/reactions.go +++ b/vendor/github.com/nlopes/slack/reactions.go @@ -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. func (api *Client) AddReactionContext(ctx context.Context, name string, item ItemRef) error { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if name != "" { values.Set("name", name) } if item.Channel != "" { - values.Set("channel", string(item.Channel)) + values.Set("channel", item.Channel) } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } + 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 } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // 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. func (api *Client) RemoveReactionContext(ctx context.Context, name string, item ItemRef) error { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if name != "" { values.Set("name", name) } if item.Channel != "" { - values.Set("channel", string(item.Channel)) + values.Set("channel", item.Channel) } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } + 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 } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // 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 func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if item.Channel != "" { - values.Set("channel", string(item.Channel)) + values.Set("channel", item.Channel) } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } if params.Full != DEFAULT_REACTIONS_FULL { values.Set("full", strconv.FormatBool(params.Full)) } + 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 } 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. func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, *Paging, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if params.User != DEFAULT_REACTIONS_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 { values.Add("full", strconv.FormatBool(params.Full)) } + 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 { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/rtm.go b/vendor/github.com/nlopes/slack/rtm.go index bb3cde1b..41a136eb 100644 --- a/vendor/github.com/nlopes/slack/rtm.go +++ b/vendor/github.com/nlopes/slack/rtm.go @@ -3,16 +3,34 @@ package slack import ( "context" "encoding/json" - "fmt" "net/url" + "sync" "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. // // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. 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. @@ -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. func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) { 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 { - return nil, "", fmt.Errorf("post: %s", err) - } - if !response.Ok { - return nil, "", response.Error + return nil, "", err } + 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. // // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. 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. func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) { 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 { - return nil, "", fmt.Errorf("post: %s", err) - } - if !response.Ok { - return nil, "", response.Error + api.Debugf("Failed to connect to RTM: %s", err) + return nil, "", err } + api.Debugln("Using URL:", response.Info.URL) - return &response.Info, response.Info.URL, nil + return &response.Info, response.Info.URL, response.Err() } -// NewRTM returns a RTM, which provides a fully managed connection to -// Slack's websocket-based Real-Time Messaging protocol. -func (api *Client) NewRTM() *RTM { - return api.NewRTMWithOptions(nil) +// 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() + } } -// NewRTMWithOptions 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. -// This also allows to configure various options available for RTM API. -func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM { +func (api *Client) NewRTM(options ...RTMOption) *RTM { result := &RTM{ Client: *api, IncomingEvents: make(chan RTMEvent, 50), outgoingMessages: make(chan OutgoingMessage, 20), - pings: make(map[int]time.Time), + pingInterval: defaultPingInterval, + pingDeadman: time.NewTimer(deadmanDuration(defaultPingInterval)), isConnected: false, wasIntentional: true, killChannel: make(chan bool), - disconnected: make(chan struct{}), + disconnected: make(chan struct{}, 1), forcePing: make(chan bool), rawEvents: make(chan json.RawMessage), idGen: NewSafeID(1), + mu: &sync.Mutex{}, } - if options != nil { - result.useRTMStart = options.UseRTMStart - } else { - result.useRTMStart = true + for _, opt := range options { + opt(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() +} diff --git a/vendor/github.com/nlopes/slack/search.go b/vendor/github.com/nlopes/slack/search.go index 0e8d65e2..e858952f 100644 --- a/vendor/github.com/nlopes/slack/search.go +++ b/vendor/github.com/nlopes/slack/search.go @@ -11,7 +11,7 @@ const ( DEFAULT_SEARCH_SORT = "score" DEFAULT_SEARCH_SORT_DIR = "desc" DEFAULT_SEARCH_HIGHLIGHT = false - DEFAULT_SEARCH_COUNT = 100 + DEFAULT_SEARCH_COUNT = 20 DEFAULT_SEARCH_PAGE = 1 ) @@ -37,17 +37,18 @@ type CtxMessage struct { } type SearchMessage struct { - Type string `json:"type"` - Channel CtxChannel `json:"channel"` - User string `json:"user"` - Username string `json:"username"` - Timestamp string `json:"ts"` - Text string `json:"text"` - Permalink string `json:"permalink"` - Previous CtxMessage `json:"previous"` - Previous2 CtxMessage `json:"previous_2"` - Next CtxMessage `json:"next"` - Next2 CtxMessage `json:"next_2"` + Type string `json:"type"` + Channel CtxChannel `json:"channel"` + User string `json:"user"` + Username string `json:"username"` + Timestamp string `json:"ts"` + Text string `json:"text"` + Permalink string `json:"permalink"` + Attachments []Attachment `json:"attachments"` + Previous CtxMessage `json:"previous"` + Previous2 CtxMessage `json:"previous_2"` + Next CtxMessage `json:"next"` + Next2 CtxMessage `json:"next_2"` } 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) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "query": {query}, } 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 { values.Add("page", strconv.Itoa(params.Page)) } + response = &searchResponseFull{} - err := post(ctx, path, values, response, api.debug) + err := postSlackMethod(ctx, api.httpclient, path, values, response, api.debug) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/slack.go b/vendor/github.com/nlopes/slack/slack.go index 754117c1..6d1e7de9 100644 --- a/vendor/github.com/nlopes/slack/slack.go +++ b/vendor/github.com/nlopes/slack/slack.go @@ -5,20 +5,47 @@ import ( "errors" "fmt" "log" + "net/http" "net/url" "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_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s" -type SlackResponse struct { - Ok bool `json:"ok"` - Error string `json:"error"` +// HTTPClient sets a custom http.Client +// deprecated: in favor of SetHTTPClient() +var HTTPClient = &http.Client{} + +var customHTTPClient HTTPRequester = HTTPClient + +// HTTPRequester defines the minimal interface needed for an http.Client to be implemented. +// +// Use it in conjunction with the SetHTTPClient function to allow for other capabilities +// like a tracing http.Client +type HTTPRequester interface { + 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 { @@ -35,41 +62,33 @@ type authTestResponseFull struct { } type Client struct { - config struct { - token string - } - info Info - debug bool + token string + info Info + debug bool + httpclient HTTPRequester } -// stdLogger is a logger interface compatible with both stdlib and some -// 3rd party loggers such as logrus. -type stdLogger interface { - Print(...interface{}) - Printf(string, ...interface{}) - Println(...interface{}) - - Fatal(...interface{}) - Fatalf(string, ...interface{}) - Fatalln(...interface{}) - - Panic(...interface{}) - Panicf(string, ...interface{}) - Panicln(...interface{}) +// Option defines an option for a Client +type Option func(*Client) - Output(int, string) error +// OptionHTTPClient - provide a custom http client to the slack client. +func OptionHTTPClient(c HTTPRequester) func(*Client) { + return func(s *Client) { + s.httpclient = c + } } -// 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 builds a slack client from the provided token and options. +func New(token string, options ...Option) *Client { + s := &Client{ + token: token, + httpclient: customHTTPClient, + } + + for _, opt := range options { + opt(s) + } -// New creates new Client. -func New(token string) *Client { - s := &Client{} - s.config.token = token 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 func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, error error) { + api.Debugf("Challenging auth...") 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 { + api.Debugf("failed to test for auth: %s", err) return nil, err } if !responseFull.Ok { + api.Debugf("auth response was not Ok: %s", responseFull.Error) return nil, errors.New(responseFull.Error) } + + api.Debugf("Auth challenge was successful with response %+v", responseFull.AuthTestResponse) return &responseFull.AuthTestResponse, nil } @@ -97,16 +121,18 @@ func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestRespo func (api *Client) SetDebug(debug bool) { api.debug = debug 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{}) { if api.debug { logger.Output(2, fmt.Sprintf(format, v...)) } } +// Debugln print a debug line. func (api *Client) Debugln(v ...interface{}) { if api.debug { logger.Output(2, fmt.Sprintln(v...)) diff --git a/vendor/github.com/nlopes/slack/slash.go b/vendor/github.com/nlopes/slack/slash.go new file mode 100644 index 00000000..f62065a2 --- /dev/null +++ b/vendor/github.com/nlopes/slack/slash.go @@ -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 +} diff --git a/vendor/github.com/nlopes/slack/stars.go b/vendor/github.com/nlopes/slack/stars.go index cf4a4a11..c1e2f6cb 100644 --- a/vendor/github.com/nlopes/slack/stars.go +++ b/vendor/github.com/nlopes/slack/stars.go @@ -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 { values := url.Values{ "channel": {channel}, - "token": {api.config.token}, + "token": {api.token}, } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } + 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 } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // 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 { values := url.Values{ "channel": {channel}, - "token": {api.config.token}, + "token": {api.token}, } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } + 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 } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // 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 func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, *Paging, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if params.User != DEFAULT_STARS_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 { values.Add("page", strconv.Itoa(params.Page)) } + 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 { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/team.go b/vendor/github.com/nlopes/slack/team.go index e70ac57e..b6e341eb 100644 --- a/vendor/github.com/nlopes/slack/team.go +++ b/vendor/github.com/nlopes/slack/team.go @@ -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{} - err := post(ctx, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -81,9 +81,9 @@ func teamRequest(ctx context.Context, path string, values url.Values, debug bool 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{} - err := post(ctx, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { return nil, err } @@ -95,9 +95,9 @@ func billableInfoRequest(ctx context.Context, path string, values url.Values, de 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{} - err := post(ctx, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { 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 func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) { 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 { 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 func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, *Paging, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } if params.Count != DEFAULT_LOGINS_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 { 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 { 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) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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. @@ -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 func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[string]BillingActive, error) { 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) } diff --git a/vendor/github.com/nlopes/slack/usergroups.go b/vendor/github.com/nlopes/slack/usergroups.go index de9f9864..1e2b6442 100644 --- a/vendor/github.com/nlopes/slack/usergroups.go +++ b/vendor/github.com/nlopes/slack/usergroups.go @@ -40,9 +40,9 @@ type userGroupResponseFull struct { SlackResponse } -func userGroupRequest(ctx context.Context, 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{} - err := post(ctx, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, debug) if err != nil { 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 func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "name": {userGroup.Name}, } @@ -76,7 +76,7 @@ func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGro 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 { 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 func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { 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 func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { 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 func (api *Client) GetUserGroupsContext(ctx context.Context) ([]UserGroup, error) { 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 { 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 func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "usergroup": {userGroup.ID}, } @@ -163,7 +163,7 @@ func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGro 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 { 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 func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup string) ([]string, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { 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 func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup string, members string) (UserGroup, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "usergroup": {userGroup}, "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 { return UserGroup{}, err } diff --git a/vendor/github.com/nlopes/slack/users.go b/vendor/github.com/nlopes/slack/users.go index f7fbd0d2..0dd20db5 100644 --- a/vendor/github.com/nlopes/slack/users.go +++ b/vendor/github.com/nlopes/slack/users.go @@ -5,41 +5,103 @@ import ( "encoding/json" "errors" "net/url" + "strconv" ) const ( DEFAULT_USER_PHOTO_CROP_X = -1 DEFAULT_USER_PHOTO_CROP_Y = -1 DEFAULT_USER_PHOTO_CROP_W = -1 + errPaginationComplete = errorString("pagination complete") ) // UserProfile contains all the information details of a given user type UserProfile struct { - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - RealName string `json:"real_name"` - RealNameNormalized string `json:"real_name_normalized"` - DisplayName string `json:"display_name"` - DisplayNameNormalized string `json:"display_name_normalized"` - Email string `json:"email"` - Skype string `json:"skype"` - Phone string `json:"phone"` - Image24 string `json:"image_24"` - Image32 string `json:"image_32"` - Image48 string `json:"image_48"` - Image72 string `json:"image_72"` - Image192 string `json:"image_192"` - ImageOriginal string `json:"image_original"` - Title string `json:"title"` - BotID string `json:"bot_id,omitempty"` - ApiAppID string `json:"api_app_id,omitempty"` - StatusText string `json:"status_text,omitempty"` - StatusEmoji string `json:"status_emoji,omitempty"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + RealName string `json:"real_name"` + RealNameNormalized string `json:"real_name_normalized"` + DisplayName string `json:"display_name"` + DisplayNameNormalized string `json:"display_name_normalized"` + Email string `json:"email"` + Skype string `json:"skype"` + Phone string `json:"phone"` + Image24 string `json:"image_24"` + Image32 string `json:"image_32"` + Image48 string `json:"image_48"` + Image72 string `json:"image_72"` + Image192 string `json:"image_192"` + ImageOriginal string `json:"image_original"` + Title string `json:"title"` + BotID string `json:"bot_id,omitempty"` + ApiAppID string `json:"api_app_id,omitempty"` + StatusText string `json:"status_text,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 type User struct { ID string `json:"id"` + TeamID string `json:"team_id"` Name string `json:"name"` Deleted bool `json:"deleted"` Color string `json:"color"` @@ -54,9 +116,12 @@ type User struct { IsPrimaryOwner bool `json:"is_primary_owner"` IsRestricted bool `json:"is_restricted"` IsUltraRestricted bool `json:"is_ultra_restricted"` + IsStranger bool `json:"is_stranger"` + IsAppUser bool `json:"is_app_user"` Has2FA bool `json:"has_2fa"` HasFiles bool `json:"has_files"` Presence string `json:"presence"` + Locale string `json:"locale"` } // UserPresence contains details about a user online status @@ -103,10 +168,11 @@ type TeamIdentity struct { } type userResponseFull struct { - Members []User `json:"members,omitempty"` // ListUsers - User `json:"user,omitempty"` // GetUserInfo - UserPresence // GetUserPresence + Members []User `json:"members,omitempty"` + User `json:"user,omitempty"` + UserPresence SlackResponse + Metadata ResponseMetadata `json:"response_metadata"` } 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{} - err := post(ctx, path, values, response, debug) + err := postForm(ctx, client, SLACK_API+path, values, response, debug) if err != nil { 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. func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*UserPresence, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { 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 func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 { return nil, err } 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) func (api *Client) GetUsers() ([]User, error) { return api.GetUsersContext(context.Background()) } // 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) { + var ( + p UserPagination + ) + + for p = api.GetUsersPaginated(); !p.Done(err); p, err = p.Next(ctx) { + results = append(results, p.Users...) + } + + 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.config.token}, - "presence": {"1"}, + "token": {api.token}, + "email": {email}, } - response, err := userRequest(ctx, "users.list", values, api.debug) + response, err := userRequest(ctx, api.httpclient, "users.lookupByEmail", values, api.debug) if err != nil { return nil, err } - return response.Members, nil + return &response.User, nil } // 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 -func (api *Client) SetUserAsActiveContext(ctx context.Context) error { +func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) { 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 } @@ -211,15 +385,12 @@ func (api *Client) SetUserPresence(presence string) error { // SetUserPresenceContext changes the currently authenticated user presence with a custom context func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) error { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, "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 @@ -230,10 +401,11 @@ func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) { // GetUserIdentityContext will retrieve user info available per identity scopes with a custom context func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityResponse, error) { values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } 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 { 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 { response := &SlackResponse{} values := url.Values{ - "token": {api.config.token}, + "token": {api.token}, } 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 { - values.Add("crop_y", string(params.CropY)) + values.Add("crop_y", strconv.Itoa(params.CropX)) } 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 { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // 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 { response := &SlackResponse{} 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 { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // 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{ - "token": {api.config.token}, + "token": {api.token}, "profile": {string(profile)}, } response := &userResponseFull{} - - if err = post(ctx, "users.profile.set", values, response, api.debug); err != nil { + if err = postForm(ctx, api.httpclient, SLACK_API+"users.profile.set", values, response, api.debug); err != nil { return err } @@ -359,3 +528,31 @@ func (api *Client) UnsetUserCustomStatus() error { func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error { 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 +} diff --git a/vendor/github.com/nlopes/slack/webhooks.go b/vendor/github.com/nlopes/slack/webhooks.go new file mode 100644 index 00000000..870a8d8b --- /dev/null +++ b/vendor/github.com/nlopes/slack/webhooks.go @@ -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 +} diff --git a/vendor/github.com/nlopes/slack/websocket.go b/vendor/github.com/nlopes/slack/websocket.go index 77906e07..242acf40 100644 --- a/vendor/github.com/nlopes/slack/websocket.go +++ b/vendor/github.com/nlopes/slack/websocket.go @@ -3,9 +3,10 @@ package slack import ( "encoding/json" "errors" + "sync" "time" - "golang.org/x/net/websocket" + "github.com/gorilla/websocket" ) const ( @@ -19,8 +20,9 @@ const ( // // Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions) type RTM struct { - idGen IDGenerator - pings map[int]time.Time + idGen IDGenerator + pingInterval time.Duration + pingDeadman *time.Timer // Connection life-cycle conn *websocket.Conn @@ -44,6 +46,13 @@ type RTM struct { // rtm.start to connect to Slack, otherwise it will use // rtm.connect 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 @@ -60,9 +69,17 @@ type RTMOptions struct { // Disconnect and wait, blocking until a successful disconnection. func (rtm *RTM) Disconnect() error { - // this channel is always closed on disconnect. lets the ManagedConnection() function - // properly clean up. - close(rtm.disconnected) + // avoid RTM disconnect race conditions + rtm.mu.Lock() + 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 { return errors.New("Invalid call to Disconnect - Slack API is already disconnected") @@ -72,12 +89,6 @@ func (rtm *RTM) Disconnect() error { 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 // "startrtm", holding all channels, groups and other metadata needed // 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 } + +func (rtm *RTM) resetDeadman() { + timerReset(rtm.pingDeadman, deadmanDuration(rtm.pingInterval)) +} + +func deadmanDuration(d time.Duration) time.Duration { + return d * 4 +} diff --git a/vendor/github.com/nlopes/slack/websocket_internals.go b/vendor/github.com/nlopes/slack/websocket_internals.go index 2a8abe6e..e8374b0d 100644 --- a/vendor/github.com/nlopes/slack/websocket_internals.go +++ b/vendor/github.com/nlopes/slack/websocket_internals.go @@ -63,6 +63,13 @@ func (m *MessageTooLongEvent) Error() string { 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 type OutgoingErrorEvent struct { Message OutgoingMessage diff --git a/vendor/github.com/nlopes/slack/websocket_managed_conn.go b/vendor/github.com/nlopes/slack/websocket_managed_conn.go index fec07fd7..b6d1bfc8 100644 --- a/vendor/github.com/nlopes/slack/websocket_managed_conn.go +++ b/vendor/github.com/nlopes/slack/websocket_managed_conn.go @@ -4,10 +4,11 @@ import ( "encoding/json" "fmt" "io" + "net/http" "reflect" "time" - "golang.org/x/net/websocket" + "github.com/gorilla/websocket" ) // 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. func (rtm *RTM) ManageConnection() { - var connectionCount int - for { - connectionCount++ + var ( + err error + info *Info + conn *websocket.Conn + ) + + for connectionCount := 0; ; connectionCount++ { // start trying to connect // the returned err is already passed onto the IncomingEvents channel - info, conn, err := rtm.connect(connectionCount, rtm.useRTMStart) - // if err != nil then the connection is sucessful - otherwise it is - // fatal - if err != nil { + if info, conn, err = rtm.connect(connectionCount, rtm.useRTMStart); err != nil { + // when the connection is unsuccessful its fatal, and we need to bail out. + rtm.Debugf("Failed to connect with RTM on try %d: %s", connectionCount, err) 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.mu.Unlock() + rtm.IncomingEvents <- RTMEvent{"connected", &ConnectedEvent{ ConnectionCount: connectionCount, Info: info, }} - rtm.conn = conn - rtm.isConnected = true + rtm.Debugf("RTM connection succeeded on try %d", connectionCount) keepRunning := make(chan bool) // we're now connected (or have failed fatally) so we can set up @@ -50,7 +61,7 @@ func (rtm *RTM) ManageConnection() { go rtm.handleIncomingEvents(keepRunning) // 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 // 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, // otherwise it uses rtm.start. 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 // to connect to slack again boff := &backoff{ @@ -87,10 +104,14 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke if err == 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{}} - return nil, nil, sErr + return nil, nil, err + default: } // 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. select { - case _ = <-rtm.disconnected: + case <-rtm.disconnected: rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: true}} return nil, nil, fmt.Errorf("disconnect received while trying to connect") 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, // 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 -func (rtm *RTM) startRTMAndDial(useRTMStart bool) (*Info, *websocket.Conn, error) { - var info *Info - var url string - var err error +func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn, err error) { + var ( + url string + ) if useRTMStart { + rtm.Debugf("Starting RTM") info, url, err = rtm.StartRTM() } else { + rtm.Debugf("Connecting to RTM") info, url, err = rtm.ConnectRTM() } if err != nil { + rtm.Debugf("Failed to start or connect to RTM: %s", 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. - 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 { + rtm.Debugf("Failed to dial to the websocket: %s", err) return nil, nil, 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 // outgoingMessages channel. This also handles incoming raw events from the RTM // rawEvents channel. -func (rtm *RTM) handleEvents(keepRunning chan bool, interval time.Duration) { - ticker := time.NewTicker(interval) +func (rtm *RTM) handleEvents(keepRunning chan bool) { + ticker := time.NewTicker(rtm.pingInterval) defer ticker.Stop() for { select { @@ -172,7 +204,12 @@ func (rtm *RTM) handleEvents(keepRunning chan bool, interval time.Duration) { case intentional := <-rtm.killChannel: _ = rtm.killConnection(keepRunning, intentional) 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: err := rtm.ping() if err != nil { @@ -190,7 +227,11 @@ func (rtm *RTM) handleEvents(keepRunning chan bool, interval time.Duration) { rtm.sendOutgoingMessage(msg) // listen for incoming messages that need to be parsed 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: return 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 { return err } - if err := websocket.JSON.Send(rtm.conn, msg); err != nil { + if err := rtm.conn.WriteJSON(msg); err != nil { return err } // remove write deadline @@ -258,9 +301,7 @@ func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) { func (rtm *RTM) ping() error { id := rtm.idGen.Next() rtm.Debugln("Sending PING ", id) - rtm.pings[id] = time.Now() - - msg := &Ping{ID: id, Type: "ping"} + msg := &Ping{ID: id, Type: "ping", Timestamp: time.Now().Unix()} if err := rtm.sendWithDeadline(msg); err != nil { 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. // 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{} - err := websocket.JSON.Receive(rtm.conn, &event) - if err == io.EOF { + err := rtm.conn.ReadJSON(&event) + switch { + case err == io.ErrUnexpectedEOF: // 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 // 'PING' message - // trigger a 'PING' to detect pontential websocket disconnect + // trigger a 'PING' to detect potential websocket disconnect rtm.forcePing <- true - return - } else if err != nil { + case err != nil: + // All other errors from ReadJSON come from NextReader, and should + // kill the read loop and force a reconnect. rtm.IncomingEvents <- RTMEvent{"incoming_error", &IncomingEventError{ ErrorObj: err, }} - // force a ping here too? - return - } else if len(event) == 0 { + rtm.killChannel <- false + return err + case len(event) == 0: rtm.Debugln("Received empty event") - return + default: + rtm.Debugln("Incoming Event:", string(event[:])) + rtm.rawEvents <- event } - rtm.Debugln("Incoming Event:", string(event[:])) - rtm.rawEvents <- event + return nil } // handleRawEvent takes a raw JSON message received from the slack websocket // 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{} err := json.Unmarshal(rawEvent, event) if err != nil { rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} - return + return "" } + switch event.Type { - case "": + case rtmEventTypeAck: rtm.handleAck(rawEvent) - case "hello": + case rtmEventTypeHello: rtm.IncomingEvents <- RTMEvent{"hello", &HelloEvent{}} - case "pong": + case rtmEventTypePong: 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") default: rtm.handleEvent(event.Type, rawEvent) } + + return event.Type } // handleAck handles an incoming 'ACK' message. @@ -331,7 +382,13 @@ func (rtm *RTM) handleAck(event json.RawMessage) { if ack.Ok { rtm.IncomingEvents <- RTMEvent{"ack", ack} } 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 { 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 // connection's latency. func (rtm *RTM) handlePong(event json.RawMessage) { - pong := &Pong{} - if err := json.Unmarshal(event, pong); err != nil { - rtm.Debugln("RTM Error unmarshalling 'pong' event:", err) + var ( + p Pong + ) + + 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)) return } - if pingTime, exists := rtm.pings[pong.ReplyTo]; exists { - latency := time.Since(pingTime) - rtm.IncomingEvents <- RTMEvent{"latency_report", &LatencyReport{Value: latency}} - delete(rtm.pings, pong.ReplyTo) - } else { - rtm.Debugln("RTM Error - unmatched 'pong' event:", string(event)) - } + + latency := time.Since(time.Unix(p.Timestamp, 0)) + rtm.IncomingEvents <- RTMEvent{"latency_report", &LatencyReport{Value: latency}} } // 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 // IncomingEvents channel. func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { - v, exists := eventMapping[typeStr] + v, exists := EventMapping[typeStr] if !exists { rtm.Debugf("RTM Error, received unmapped event %q: %s\n", typeStr, string(event)) err := fmt.Errorf("RTM Error: Received unmapped event %q: %s\n", typeStr, string(event)) @@ -382,10 +440,10 @@ func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { 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 // target for the matching event type. -var eventMapping = map[string]interface{}{ +var EventMapping = map[string]interface{}{ "message": MessageEvent{}, "presence_change": PresenceChangeEvent{}, "user_typing": UserTypingEvent{}, @@ -463,4 +521,7 @@ var eventMapping = map[string]interface{}{ "accounts_changed": AccountsChangedEvent{}, "reconnect_url": ReconnectUrlEvent{}, + + "member_joined_channel": MemberJoinedChannelEvent{}, + "member_left_channel": MemberLeftChannelEvent{}, } diff --git a/vendor/github.com/nlopes/slack/websocket_misc.go b/vendor/github.com/nlopes/slack/websocket_misc.go index ad283ea1..16f48c74 100644 --- a/vendor/github.com/nlopes/slack/websocket_misc.go +++ b/vendor/github.com/nlopes/slack/websocket_misc.go @@ -80,7 +80,7 @@ type EmojiChangedEvent struct { SubType string `json:"subtype"` Name string `json:"name"` Names []string `json:"names"` - Value string `json:"value"` + Value string `json:"value"` EventTimestamp string `json:"event_ts"` } @@ -119,3 +119,22 @@ type ReconnectUrlEvent struct { Type string `json:"type"` 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"` +} diff --git a/vendor/github.com/nlopes/slack/websocket_proxy.go b/vendor/github.com/nlopes/slack/websocket_proxy.go deleted file mode 100644 index 27f1cf99..00000000 --- a/vendor/github.com/nlopes/slack/websocket_proxy.go +++ /dev/null @@ -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) -} diff --git a/vendor/github.com/pkg/errors/.gitignore b/vendor/github.com/pkg/errors/.gitignore new file mode 100644 index 00000000..daf913b1 --- /dev/null +++ b/vendor/github.com/pkg/errors/.gitignore @@ -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 diff --git a/vendor/github.com/pkg/errors/.travis.yml b/vendor/github.com/pkg/errors/.travis.yml new file mode 100644 index 00000000..588ceca1 --- /dev/null +++ b/vendor/github.com/pkg/errors/.travis.yml @@ -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 ./... diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE new file mode 100644 index 00000000..835ba3e7 --- /dev/null +++ b/vendor/github.com/pkg/errors/LICENSE @@ -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. diff --git a/vendor/github.com/pkg/errors/README.md b/vendor/github.com/pkg/errors/README.md new file mode 100644 index 00000000..273db3c9 --- /dev/null +++ b/vendor/github.com/pkg/errors/README.md @@ -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 diff --git a/vendor/github.com/pkg/errors/appveyor.yml b/vendor/github.com/pkg/errors/appveyor.yml new file mode 100644 index 00000000..a932eade --- /dev/null +++ b/vendor/github.com/pkg/errors/appveyor.yml @@ -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 diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go new file mode 100644 index 00000000..842ee804 --- /dev/null +++ b/vendor/github.com/pkg/errors/errors.go @@ -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 +} diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go new file mode 100644 index 00000000..6b1f2891 --- /dev/null +++ b/vendor/github.com/pkg/errors/stack.go @@ -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 +} |