Add vendor folder

This commit is contained in:
erroneousboat 2018-07-21 13:21:22 +02:00
parent d326f4e3d0
commit 61561062ff
30 changed files with 876 additions and 390 deletions

View File

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

View File

@ -1,3 +1,9 @@
### 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

13
vendor/github.com/nlopes/slack/Gopkg.toml generated vendored Normal file
View File

@ -0,0 +1,13 @@
ignored = ["github.com/lusis/slack-test"]
[[constraint]]
name = "github.com/gorilla/websocket"
version = "1.2.0"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
[prune]
go-tests = true
unused-packages = true

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

@ -62,6 +62,7 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi
"last_name": {lastName},
"ultra_restricted": {"1"},
"token": {api.token},
"resend": {"true"},
"set_active": {"true"},
"_attempts": {"1"},
}
@ -88,6 +89,7 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe
"last_name": {lastName},
"restricted": {"1"},
"token": {api.token},
"resend": {"true"},
"set_active": {"true"},
"_attempts": {"1"},
}

View File

@ -78,6 +78,7 @@ type Attachment struct {
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

@ -52,11 +52,8 @@ func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string)
"channel": {channelID},
}
if _, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug); err != nil {
return err
}
return nil
_, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug)
return err
}
// UnarchiveChannel unarchives the given channel
@ -73,11 +70,8 @@ func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string
"channel": {channelID},
}
if _, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug); err != nil {
return err
}
return nil
_, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug)
return err
}
// CreateChannel creates a channel with the given name and returns a *Channel
@ -247,11 +241,8 @@ func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, us
"user": {user},
}
if _, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug); err != nil {
return err
}
return nil
_, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug)
return err
}
// GetChannels retrieves all the channels
@ -297,11 +288,8 @@ func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts
"ts": {ts},
}
if _, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug); err != nil {
return err
}
return nil
_, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug)
return err
}
// RenameChannel renames a given channel

View File

@ -3,7 +3,6 @@ package slack
import (
"context"
"encoding/json"
"errors"
"net/url"
"strings"
)
@ -24,15 +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 {
Username string `json:"user_name"`
Username string `json:"username"`
AsUser bool `json:"as_user"`
Parse string `json:"parse"`
ThreadTimestamp string `json:"thread_ts"`
@ -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.token, channel, options...)
if err != nil {
return "", err
}
values.Add("user", userID)
response, err := chatRequest(ctx, api.httpclient, 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)
}
// UpdateMessageContext 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))
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.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, api.httpclient, channel, values, api.debug)
if err != nil {
if err = post(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, client HTTPRequester, path string, values url.Values, debug bool) (*chatResponseFull, error) {
response := &chatResponseFull{}
err := post(ctx, client, 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 (
@ -232,7 +226,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 +236,17 @@ 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
}
}
// MsgOptionUpdate updates a message based on the timestamp.
func MsgOptionUpdate(timestamp string) MsgOption {
return func(config *sendConfig) error {
@ -269,6 +275,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 {
@ -328,11 +342,48 @@ 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
@ -344,7 +395,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

@ -83,7 +83,7 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge
values.Add("cursor", params.Cursor)
}
if params.Limit != 0 {
values.Add("limit", string(params.Limit))
values.Add("limit", strconv.Itoa(params.Limit))
}
response := struct {
Members []string `json:"members"`
@ -116,10 +116,8 @@ func (api *Client) ArchiveConversationContext(ctx context.Context, channelID str
if err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
return response.Err()
}
// UnArchiveConversation reverses conversation archival
@ -138,10 +136,8 @@ func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID s
if err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
return response.Err()
}
// SetTopicOfConversation sets the topic for a conversation
@ -164,10 +160,8 @@ func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID,
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.Channel, nil
return response.Channel, response.Err()
}
// SetPurposeOfConversation sets the purpose for a conversation
@ -190,10 +184,8 @@ func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelI
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.Channel, nil
return response.Channel, response.Err()
}
// RenameConversation renames a conversation
@ -216,10 +208,8 @@ func (api *Client) RenameConversationContext(ctx context.Context, channelID, cha
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.Channel, nil
return response.Channel, response.Err()
}
// InviteUsersToConversation invites users to a channel
@ -242,10 +232,8 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return response.Channel, nil
return response.Channel, response.Err()
}
// KickUserFromConversation removes a user from a conversation
@ -265,10 +253,8 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI
if err != nil {
return err
}
if !response.Ok {
return errors.New(response.Error)
}
return nil
return response.Err()
}
// CloseConversation closes a direct message or multi-person direct message
@ -292,10 +278,8 @@ func (api *Client) CloseConversationContext(ctx context.Context, channelID strin
if err != nil {
return false, false, err
}
if !response.Ok {
return false, false, errors.New(response.Error)
}
return response.NoOp, response.AlreadyClosed, nil
return response.NoOp, response.AlreadyClosed, response.Err()
}
// CreateConversation initiates a public or private channel-based conversation
@ -315,10 +299,8 @@ func (api *Client) CreateConversationContext(ctx context.Context, channelName st
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return &response.Channel, nil
return &response.Channel, response.Err()
}
// GetConversationInfo retrieves information about a conversation
@ -338,10 +320,8 @@ func (api *Client) GetConversationInfoContext(ctx context.Context, channelID str
if err != nil {
return nil, err
}
if !response.Ok {
return nil, errors.New(response.Error)
}
return &response.Channel, nil
return &response.Channel, response.Err()
}
// LeaveConversation leaves a conversation
@ -357,11 +337,7 @@ func (api *Client) LeaveConversationContext(ctx context.Context, channelID strin
}
response, err := channelRequest(ctx, api.httpclient, "conversations.leave", values, api.debug)
if err != nil {
return false, err
}
return response.NotInChannel, nil
return response.NotInChannel, err
}
type GetConversationRepliesParameters struct {
@ -393,7 +369,7 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge
values.Add("latest", params.Latest)
}
if params.Limit != 0 {
values.Add("limit", string(params.Limit))
values.Add("limit", strconv.Itoa(params.Limit))
}
if params.Oldest != "" {
values.Add("oldest", params.Oldest)
@ -416,10 +392,8 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge
if err != nil {
return nil, false, "", err
}
if !response.Ok {
return nil, false, "", errors.New(response.Error)
}
return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, nil
return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, response.Err()
}
type GetConversationsParameters struct {
@ -444,7 +418,7 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve
values.Add("cursor", params.Cursor)
}
if params.Limit != 0 {
values.Add("limit", string(params.Limit))
values.Add("limit", strconv.Itoa(params.Limit))
}
if params.Types != nil {
values.Add("types", strings.Join(params.Types, ","))
@ -458,10 +432,8 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve
if err != nil {
return nil, "", err
}
if !response.Ok {
return nil, "", errors.New(response.Error)
}
return response.Channels, response.ResponseMetaData.NextCursor, nil
return response.Channels, response.ResponseMetaData.NextCursor, response.Err()
}
type OpenConversationParameters struct {
@ -497,10 +469,8 @@ func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConv
if err != nil {
return nil, false, false, err
}
if !response.Ok {
return nil, false, false, errors.New(response.Error)
}
return response.Channel, response.NoOp, response.AlreadyOpen, nil
return response.Channel, response.NoOp, response.AlreadyOpen, response.Err()
}
// JoinConversation joins an existing conversation
@ -523,8 +493,8 @@ func (api *Client) JoinConversationContext(ctx context.Context, channelID string
if err != nil {
return nil, "", nil, err
}
if !response.Ok {
return nil, "", nil, errors.New(response.Error)
if response.Err() != nil {
return nil, "", nil, response.Err()
}
var warnings []string
if response.ResponseMetaData != nil {
@ -573,7 +543,7 @@ func (api *Client) GetConversationHistoryContext(ctx context.Context, params *Ge
values.Add("latest", params.Latest)
}
if params.Limit != 0 {
values.Add("limit", string(params.Limit))
values.Add("limit", strconv.Itoa(params.Limit))
}
if params.Oldest != "" {
values.Add("oldest", params.Oldest)

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

@ -0,0 +1,107 @@
package slack
import (
"context"
"errors"
"encoding/json"
)
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

@ -64,10 +64,8 @@ func (api *Client) EndDNDContext(ctx context.Context) error {
if err := post(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

View File

@ -244,9 +244,9 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam
values.Add("content", params.Content)
err = postForm(ctx, api.httpclient, SLACK_API+"files.upload", values, response, api.debug)
} else if params.File != "" {
err = postLocalWithMultipartResponse(ctx, api.httpclient, SLACK_API+"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, api.httpclient, SLACK_API+"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
@ -273,11 +273,8 @@ func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, comment
"file": {fileID},
"id": {commentID},
}
if _, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug); err != nil {
return err
}
return nil
_, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug)
return err
}
// DeleteFile deletes a file
@ -292,11 +289,8 @@ func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err er
"file": {fileID},
}
if _, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug); err != nil {
return err
}
return nil
_, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug)
return err
}
// RevokeFilePublicURL disables public/external sharing for a file

View File

@ -53,9 +53,6 @@ func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error
}
_, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api.debug)
if err != nil {
return err
}
return err
}
@ -72,10 +69,7 @@ func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) erro
}
_, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// CreateGroup creates a private group
@ -215,11 +209,8 @@ func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err err
"channel": {group},
}
if _, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug); err != nil {
return err
}
return nil
_, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug)
return err
}
// KickUserFromGroup kicks a user from a group
@ -235,11 +226,8 @@ func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user str
"user": {user},
}
if _, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug); err != nil {
return err
}
return nil
_, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug)
return err
}
// GetGroups retrieves all groups
@ -300,11 +288,8 @@ func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string
"ts": {ts},
}
if _, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug); err != nil {
return err
}
return nil
_, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug)
return err
}
// OpenGroup opens a private group

View File

@ -87,18 +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.token},
"channel": {channel},
"ts": {ts},
}
_, err = imRequest(ctx, api.httpclient, "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

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

View File

@ -8,6 +8,7 @@ type OutgoingMessage struct {
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,7 +27,7 @@ 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"`
@ -89,6 +90,7 @@ type Msg struct {
// slash commands and interactive messages
ResponseType string `json:"response_type,omitempty"`
ReplaceOriginal bool `json:"replace_original,omitempty"`
DeleteOriginal bool `json:"delete_original,omitempty"`
}
// Icon is used for bot messages
@ -129,14 +131,18 @@ type Pong struct {
// 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
@ -150,3 +156,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,15 +19,41 @@ import (
"time"
)
type WebResponse struct {
Ok bool `json:"ok"`
Error *WebError `json:"error"`
type SlackResponse struct {
Ok bool `json:"ok"`
Error string `json:"error"`
}
type WebError string
func (t SlackResponse) Err() error {
if t.Ok {
return nil
}
func (s WebError) Error() string {
return string(s)
// 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
}
return errors.New(t.Error)
}
// StatusCodeError represents an http response error.
// type httpStatusCode interface { HTTPStatusCode() int } to handle it.
type statusCodeError struct {
Code int
Status string
}
func (t statusCodeError) Error() string {
// TODO: this is a bad error string, should clean it up with a breaking changes
// merger.
return fmt.Sprintf("Slack server error: %s.", t.Status)
}
func (t statusCodeError) HTTPStatusCode() int {
return t.Code
}
type RateLimitedError struct {
@ -63,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
@ -74,7 +101,7 @@ 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, client HTTPRequester, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error {
@ -113,20 +140,13 @@ func postWithMultipartResponse(ctx context.Context, client HTTPRequester, path,
// 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, 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")
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 {
@ -145,10 +165,31 @@ func postForm(ctx context.Context, client HTTPRequester, endpoint string, values
// 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 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)
}
func postForm(ctx context.Context, client HTTPRequester, endpoint string, values url.Values, intf interface{}, debug bool) error {
reqBody := strings.NewReader(values.Encode())
req, err := http.NewRequest("POST", endpoint, reqBody)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return doPost(ctx, client, req, intf, debug)
}
func post(ctx context.Context, client HTTPRequester, path string, values url.Values, intf interface{}, debug bool) error {
@ -180,3 +221,9 @@ func okJsonHandler(rw http.ResponseWriter, r *http.Request) {
})
rw.Write(response)
}
type errorString string
func (t errorString) Error() string {
return string(t)
}

View File

@ -24,23 +24,21 @@ func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemR
"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, 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
@ -55,23 +53,21 @@ func (api *Client) RemovePinContext(ctx context.Context, channel string, item It
"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, 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.

View File

@ -142,26 +142,24 @@ func (api *Client) AddReactionContext(ctx context.Context, name string, item Ite
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, 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.
@ -178,26 +176,24 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item
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, 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.
@ -211,16 +207,16 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params
"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))

View File

@ -3,9 +3,11 @@ package slack
import (
"context"
"encoding/json"
"fmt"
"net/url"
"sync"
"time"
"github.com/gorilla/websocket"
)
const (
@ -29,13 +31,11 @@ func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketUR
response := &infoResponseFull{}
err = post(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.
@ -48,7 +48,8 @@ func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) {
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) {
@ -56,25 +57,35 @@ func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocket
err = post(ctx, api.httpclient, "rtm.connect", url.Values{"token": {api.token}}, response, api.debug)
if err != nil {
api.Debugf("Failed to connect to RTM: %s", err)
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()
}
// 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
}
}
// 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),
@ -87,13 +98,23 @@ func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM {
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 {

View File

@ -35,9 +35,17 @@ func SetHTTPClient(client HTTPRequester) {
customHTTPClient = client
}
type SlackResponse struct {
Ok bool `json:"ok"`
Error string `json:"error"`
// 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 {

View File

@ -6,17 +6,19 @@ import (
// 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"`
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"`
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
@ -27,6 +29,8 @@ func SlashCommandParse(r *http.Request) (s SlashCommand, err error) {
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")

View File

@ -48,23 +48,21 @@ func (api *Client) AddStarContext(ctx context.Context, channel string, item Item
"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, 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
@ -79,23 +77,21 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I
"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, 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

View File

@ -5,37 +5,97 @@ 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"`
Team string `json:"team"`
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
@ -108,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 {
@ -178,23 +239,109 @@ func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User,
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.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, api.httpclient, "users.list", values, api.debug)
if err != nil {
return nil, err
}
return response.Members, nil
return results, p.Failure(err)
}
// GetUserByEmail will retrieve the complete user information by email
@ -226,11 +373,8 @@ func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) {
"token": {api.token},
}
if _, err := userRequest(ctx, api.httpclient, "users.setActive", values, api.debug); err != nil {
return err
}
return nil
_, err = userRequest(ctx, api.httpclient, "users.setActive", values, api.debug)
return err
}
// SetUserPresence changes the currently authenticated user presence
@ -246,11 +390,7 @@ func (api *Client) SetUserPresenceContext(ctx context.Context, presence string)
}
_, err := userRequest(ctx, api.httpclient, "users.setPresence", values, api.debug)
if err != nil {
return err
}
return nil
return err
}
// GetUserIdentity will retrieve user info available per identity scopes
@ -287,23 +427,21 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params
"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, api.httpclient, SLACK_API+"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
@ -322,10 +460,8 @@ func (api *Client) DeleteUserPhotoContext(ctx context.Context) error {
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
@ -392,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 := post(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
}

View File

@ -3,6 +3,7 @@ package slack
import (
"encoding/json"
"errors"
"sync"
"time"
"github.com/gorilla/websocket"
@ -44,6 +45,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,6 +68,9 @@ type RTMOptions struct {
// Disconnect and wait, blocking until a successful disconnection.
func (rtm *RTM) Disconnect() error {
// avoid RTM disconnect race conditions
rtm.mu.Lock()
defer rtm.mu.Unlock()
// this channel is always closed on disconnect. lets the ManagedConnection() function
// properly clean up.
close(rtm.disconnected)

View File

@ -25,18 +25,26 @@ import (
//
// The defined error events are located in websocket_internals.go.
func (rtm *RTM) ManageConnection() {
var connectionCount int
var (
err error
connectionCount int
info *Info
conn *websocket.Conn
)
for {
// BEGIN SENSITIVE CODE, make sure lock is unlocked in this section.
rtm.mu.Lock()
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)
rtm.mu.Unlock()
return
}
rtm.info = info
rtm.IncomingEvents <- RTMEvent{"connected", &ConnectedEvent{
ConnectionCount: connectionCount,
@ -45,6 +53,8 @@ func (rtm *RTM) ManageConnection() {
rtm.conn = conn
rtm.isConnected = true
rtm.mu.Unlock()
// END SENSITIVE CODE
rtm.Debugf("RTM connection succeeded on try %d", connectionCount)
@ -71,6 +81,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{
@ -91,11 +107,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
@ -107,7 +126,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:
@ -124,10 +143,10 @@ 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")
@ -145,7 +164,11 @@ func (rtm *RTM) startRTMAndDial(useRTMStart bool) (*Info, *websocket.Conn, error
// Only use HTTPS for connections to prevent MITM attacks on the connection.
upgradeHeader := http.Header{}
upgradeHeader.Add("Origin", "https://api.slack.com")
conn, _, err := websocket.DefaultDialer.Dial(url, upgradeHeader)
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
@ -220,7 +243,9 @@ func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) {
case <-keepRunning:
return
default:
rtm.receiveIncomingEvent()
if err := rtm.receiveIncomingEvent(); err != nil {
return
}
}
}
}
@ -283,29 +308,33 @@ 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 := rtm.conn.ReadJSON(&event)
if err == io.EOF {
switch {
case err == io.ErrUnexpectedEOF:
// EOF's don't seem to signify a failed connection so instead we ignore
// 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
@ -381,7 +410,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))
@ -400,10 +429,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{},
@ -481,4 +510,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"`
}