4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-28 00:59:24 +00:00

Update nlopes/slack vendor

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

View File

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

View File

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

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

@ -0,0 +1,33 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
name = "github.com/gorilla/websocket"
packages = ["."]
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "888307bf47ee004aaaa4c45e6139929b4984f2253e48e382246bfb8c66f3cd65"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -7,19 +7,20 @@ This library supports most if not all of the `api.slack.com` REST
calls, as well as the Real-Time Messaging protocol over websocket, in
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -36,9 +36,9 @@ type dndTeamInfoResponse struct {
SlackResponse
}
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
}

View File

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

View File

@ -136,9 +136,9 @@ func NewGetFilesParameters() GetFilesParameters {
}
}
func fileRequest(ctx context.Context, path string, values url.Values, debug bool) (*fileResponseFull, error) {
func fileRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*fileResponseFull, error) {
response := &fileResponseFull{}
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
}

View File

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

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

@ -29,9 +29,9 @@ type IM struct {
IsUserDeleted bool `json:"is_user_deleted"`
}
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
}

View File

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

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

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

View File

@ -2,12 +2,13 @@ package slack
// OutgoingMessage is used for the realtime API, and seems incomplete.
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
}
}

View File

@ -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
}
// HTTPClient sets a custom http.Client
// deprecated: in favor of SetHTTPClient()
var HTTPClient = &http.Client{}
// 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
}
type WebResponse struct {
Ok bool `json:"ok"`
Error *WebError `json:"error"`
return errors.New(t.Error)
}
type WebError string
// StatusCodeError represents an http response error.
// type httpStatusCode interface { HTTPStatusCode() int } to handle it.
type statusCodeError struct {
Code int
Status string
}
func (s WebError) Error() string {
return string(s)
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 (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,51 +140,68 @@ func postWithMultipartResponse(ctx context.Context, path, name, fieldname string
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
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 {
func doPost(ctx context.Context, client HTTPRequester, req *http.Request, intf interface{}, debug bool) error {
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != http.StatusOK {
logResponse(resp, debug)
return statusCodeError{Code: resp.StatusCode, Status: resp.Status}
}
return parseResponseBody(resp.Body, intf, debug)
}
// post JSON.
func postJSON(ctx context.Context, client HTTPRequester, endpoint, token string, json []byte, intf interface{}, debug bool) error {
reqBody := bytes.NewBuffer(json)
req, err := http.NewRequest("POST", endpoint, reqBody)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return doPost(ctx, client, req, intf, debug)
}
// post a url encoded form.
func postForm(ctx context.Context, client HTTPRequester, endpoint string, values url.Values, intf interface{}, debug bool) error {
reqBody := strings.NewReader(values.Encode())
req, err := http.NewRequest("POST", endpoint, reqBody)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req = req.WithContext(ctx)
resp, err := getHTTPClient().Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err != nil {
return err
}
return &RateLimitedError{time.Duration(retry) * time.Second}
}
// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
if resp.StatusCode != http.StatusOK {
logResponse(resp, debug)
return fmt.Errorf("Slack server error: %s.", resp.Status)
}
return parseResponseBody(resp.Body, &intf, debug)
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
func (t errorString) Error() string {
return string(t)
}
// timerReset safely reset a timer, see time.Timer.Reset for details.
func timerReset(t *time.Timer, d time.Duration) {
if !t.Stop() {
<-t.C
}
return HTTPClient
}
// SetHTTPClient allows you to specify a custom http.Client
// Use this instead of the package level HTTPClient variable if you want to use a custom client like the
// Stackdriver Trace HTTPClient https://godoc.org/cloud.google.com/go/trace#HTTPClient
func SetHTTPClient(client HTTPRequester) {
customHTTPClient = client
t.Reset(d)
}

View File

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

View File

@ -21,25 +21,24 @@ func (api *Client) AddPin(channel string, item ItemRef) error {
func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemRef) error {
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
}

View File

@ -136,31 +136,30 @@ func (api *Client) AddReaction(name string, item ItemRef) error {
// AddReactionContext adds a reaction emoji to a message, file or file comment with a custom context.
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
}

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

@ -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()
}
// RTMOption options for the managed RTM.
type RTMOption func(*RTM)
// RTMOptionUseStart as of 11th July 2017 you should prefer setting this to false, see:
// https://api.slack.com/changelog/2017-04-start-using-rtm-connect-and-stop-using-rtm-start
func RTMOptionUseStart(b bool) RTMOption {
return func(rtm *RTM) {
rtm.useRTMStart = b
}
}
// RTMOptionDialer takes a gorilla websocket Dialer and uses it as the
// Dialer when opening the websocket for the RTM connection.
func RTMOptionDialer(d *websocket.Dialer) RTMOption {
return func(rtm *RTM) {
rtm.dialer = d
}
}
// RTMOptionPingInterval determines how often to deliver a ping message to slack.
func RTMOptionPingInterval(d time.Duration) RTMOption {
return func(rtm *RTM) {
rtm.pingInterval = d
rtm.resetDeadman()
}
}
// NewRTM returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
func (api *Client) NewRTM() *RTM {
return api.NewRTMWithOptions(nil)
}
// NewRTMWithOptions returns a RTM, which provides a fully managed connection to
// Slack's websocket-based Real-Time Messaging protocol.
// This also allows to configure various options available for RTM API.
func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM {
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()
}

View File

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

View File

@ -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
token string
info Info
debug bool
httpclient HTTPRequester
}
// Option defines an option for a Client
type Option func(*Client)
// OptionHTTPClient - provide a custom http client to the slack client.
func OptionHTTPClient(c HTTPRequester) func(*Client) {
return func(s *Client) {
s.httpclient = c
}
info Info
debug bool
}
// stdLogger is a logger interface compatible with both stdlib and some
// 3rd party loggers such as logrus.
type stdLogger interface {
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
// New builds a slack client from the provided token and options.
func New(token string, options ...Option) *Client {
s := &Client{
token: token,
httpclient: customHTTPClient,
}
Fatal(...interface{})
Fatalf(string, ...interface{})
Fatalln(...interface{})
for _, opt := range options {
opt(s)
}
Panic(...interface{})
Panicf(string, ...interface{})
Panicln(...interface{})
Output(int, string) error
}
// SetLogger let's library users supply a logger, so that api debugging
// can be logged along with the application's debugging info.
func SetLogger(l stdLogger) {
logger = l
}
// New creates new Client.
func New(token string) *Client {
s := &Client{}
s.config.token = token
return s
}
@ -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...))

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

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

View File

@ -45,25 +45,24 @@ func (api *Client) AddStar(channel string, item ItemRef) error {
func (api *Client) AddStarContext(ctx context.Context, channel string, item ItemRef) error {
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
}

View File

@ -67,9 +67,9 @@ func NewAccessLogParameters() AccessLogParameters {
}
}
func teamRequest(ctx context.Context, path string, values url.Values, debug bool) (*TeamResponse, error) {
func teamRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*TeamResponse, error) {
response := &TeamResponse{}
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)
}

View File

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

View File

@ -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) {
values := url.Values{
"token": {api.config.token},
"presence": {"1"},
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...)
}
response, err := userRequest(ctx, "users.list", values, api.debug)
return results, p.Failure(err)
}
// GetUserByEmail will retrieve the complete user information by email
func (api *Client) GetUserByEmail(email string) (*User, error) {
return api.GetUserByEmailContext(context.Background(), email)
}
// GetUserByEmailContext will retrieve the complete user information by email with a custom context
func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*User, error) {
values := url.Values{
"token": {api.token},
"email": {email},
}
response, err := userRequest(ctx, api.httpclient, "users.lookupByEmail", values, api.debug)
if err != nil {
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
}

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

@ -0,0 +1,33 @@
package slack
import (
"github.com/pkg/errors"
"net/http"
"bytes"
"encoding/json"
)
type WebhookMessage struct {
Text string `json:"text,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
}
func PostWebhook(url string, msg *WebhookMessage) error {
raw, err := json.Marshal(msg)
if err != nil {
return errors.Wrap(err, "marshal failed")
}
response, err := http.Post(url, "application/json", bytes.NewReader(raw));
if err != nil {
return errors.Wrap(err, "failed to post webhook")
}
if response.StatusCode != http.StatusOK {
return statusCodeError{Code: response.StatusCode, Status: response.Status}
}
return nil
}

View File

@ -3,9 +3,10 @@ package slack
import (
"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
}

View File

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

View File

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

View File

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

View File

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