diff options
Diffstat (limited to 'vendor/github.com/nlopes')
38 files changed, 2038 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) -} |