pax_global_header00006660000000000000000000000064131126277710014521gustar00rootroot0000000000000052 comment=c86337c0ef2486a15edd804355d9c73d2f2caed1 golang-github-nlopes-slack-0.1.0/000077500000000000000000000000001311262777100166375ustar00rootroot00000000000000golang-github-nlopes-slack-0.1.0/.gitignore000066400000000000000000000000121311262777100206200ustar00rootroot00000000000000*.test *~ golang-github-nlopes-slack-0.1.0/.travis.yml000066400000000000000000000004131311262777100207460ustar00rootroot00000000000000language: go go: - 1.4 - 1.5 - 1.6 - 1.7 - 1.8 - 1.x - tip before_install: - export PATH=$HOME/gopath/bin:$PATH script: - go test -race ./... - go test -cover ./... matrix: allow_failures: - go: tip git: depth: 10 golang-github-nlopes-slack-0.1.0/CHANGELOG.md000066400000000000000000000006551311262777100204560ustar00rootroot00000000000000### v0.1.0 - May 28, 2017 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. Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0) ### v0.0.1 - Jul 26, 2015 If you just updated from master and it broke your implementation, please check [0.0.1](https://github.com/nlopes/slack/releases/tag/v0.0.1) golang-github-nlopes-slack-0.1.0/LICENSE000066400000000000000000000024151311262777100176460ustar00rootroot00000000000000Copyright (c) 2015, Norberto Lopes All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. golang-github-nlopes-slack-0.1.0/README.md000066400000000000000000000037201311262777100201200ustar00rootroot00000000000000Slack API in Go [![GoDoc](https://godoc.org/github.com/nlopes/slack?status.svg)](https://godoc.org/github.com/nlopes/slack) [![Build Status](https://travis-ci.org/nlopes/slack.svg)](https://travis-ci.org/nlopes/slack) =============== 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 ### v0.1.0 - May 28, 2017 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. Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0) ### CHANGELOG.md As of this version a [CHANGELOG.md](https://github.com/nlopes/slack/blob/master/README.md) is available. Please visit it for updates. ## Installing ### *go get* $ go get -u github.com/nlopes/slack ## Example ### Getting all groups ```golang import ( "fmt" "github.com/nlopes/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") // If you set debugging, it will log all requests to the console // Useful when encountering issues // api.SetDebug(true) groups, err := api.GetGroups(false) if err != nil { fmt.Printf("%s\n", err) return } for _, group := range groups { fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) } } ``` ### Getting User Information ```golang import ( "fmt" "github.com/nlopes/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") user, err := api.GetUserInfo("U023BECGF") if err != nil { fmt.Printf("%s\n", err) return } fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email) } ``` ## Minimal RTM usage: See https://github.com/nlopes/slack/blob/master/examples/websocket/websocket.go ## Contributing You are more than welcome to contribute to this project. Fork and make a Pull Request, or create an Issue if you see any problem. ## License BSD 2 Clause license golang-github-nlopes-slack-0.1.0/TODO.txt000066400000000000000000000002051311262777100201420ustar00rootroot00000000000000- Add more tests!!! - Add support to have markdown hints - See section Message Formatting at https://api.slack.com/docs/formatting golang-github-nlopes-slack-0.1.0/admin.go000066400000000000000000000107211311262777100202570ustar00rootroot00000000000000package slack import ( "errors" "fmt" "net/url" ) type adminResponse struct { OK bool `json:"ok"` Error string `json:"error"` } func adminRequest(method string, teamName string, values url.Values, debug bool) (*adminResponse, error) { adminResponse := &adminResponse{} err := parseAdminResponse(method, teamName, values, adminResponse, debug) if err != nil { return nil, err } if !adminResponse.OK { return nil, errors.New(adminResponse.Error) } return adminResponse, nil } // DisableUser disabled a user account, given a user ID func (api *Client) DisableUser(teamName string, uid string) error { values := url.Values{ "user": {uid}, "token": {api.config.token}, "set_active": {"true"}, "_attempts": {"1"}, } _, err := adminRequest("setInactive", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err) } return nil } // InviteGuest invites a user to Slack as a single-channel guest func (api *Client) InviteGuest( teamName string, channel string, firstName string, lastName string, emailAddress string, ) error { values := url.Values{ "email": {emailAddress}, "channels": {channel}, "first_name": {firstName}, "last_name": {lastName}, "ultra_restricted": {"1"}, "token": {api.config.token}, "set_active": {"true"}, "_attempts": {"1"}, } _, err := adminRequest("invite", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to invite single-channel guest: %s", err) } return nil } // InviteRestricted invites a user to Slack as a restricted account func (api *Client) InviteRestricted( teamName string, channel string, firstName string, lastName string, emailAddress string, ) error { values := url.Values{ "email": {emailAddress}, "channels": {channel}, "first_name": {firstName}, "last_name": {lastName}, "restricted": {"1"}, "token": {api.config.token}, "set_active": {"true"}, "_attempts": {"1"}, } _, err := adminRequest("invite", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to restricted account: %s", err) } return nil } // InviteToTeam invites a user to a Slack team func (api *Client) InviteToTeam( teamName string, firstName string, lastName string, emailAddress string, ) error { values := url.Values{ "email": {emailAddress}, "first_name": {firstName}, "last_name": {lastName}, "token": {api.config.token}, "set_active": {"true"}, "_attempts": {"1"}, } _, err := adminRequest("invite", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to invite to team: %s", err) } return nil } // SetRegular enables the specified user func (api *Client) SetRegular(teamName string, user string) error { values := url.Values{ "user": {user}, "token": {api.config.token}, "set_active": {"true"}, "_attempts": {"1"}, } _, err := adminRequest("setRegular", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err) } return nil } // SendSSOBindingEmail sends an SSO binding email to the specified user func (api *Client) SendSSOBindingEmail(teamName string, user string) error { values := url.Values{ "user": {user}, "token": {api.config.token}, "set_active": {"true"}, "_attempts": {"1"}, } _, err := adminRequest("sendSSOBind", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err) } return nil } // SetUltraRestricted converts a user into a single-channel guest func (api *Client) SetUltraRestricted(teamName, uid, channel string) error { values := url.Values{ "user": {uid}, "channel": {channel}, "token": {api.config.token}, "set_active": {"true"}, "_attempts": {"1"}, } _, err := adminRequest("setUltraRestricted", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to ultra-restrict account: %s", err) } return nil } // SetRestricted converts a user into a restricted account func (api *Client) SetRestricted(teamName, uid string) error { values := url.Values{ "user": {uid}, "token": {api.config.token}, "set_active": {"true"}, "_attempts": {"1"}, } _, err := adminRequest("setRestricted", teamName, values, api.debug) if err != nil { return fmt.Errorf("Failed to restrict account: %s", err) } return nil } golang-github-nlopes-slack-0.1.0/attachments.go000066400000000000000000000106011311262777100214770ustar00rootroot00000000000000package slack import "encoding/json" // AttachmentField contains information for an attachment field // An Attachment can contain multiple of these type AttachmentField struct { Title string `json:"title"` Value string `json:"value"` Short bool `json:"short"` } // AttachmentAction is a button or menu to be included in the attachment. Required when // using message buttons or menus and otherwise not useful. A maximum of 5 actions may be // provided per attachment. type AttachmentAction struct { Name string `json:"name"` // Required. Text string `json:"text"` // Required. Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger". Type string `json:"type"` // Required. Must be set to "button" or "select". Value string `json:"value,omitempty"` // Optional. DataSource string `json:"data_source,omitempty"` // Optional. MinQueryLength int `json:"min_query_length,omitempty"` // Optional. Default value is 1. Options []AttachmentActionOption `json:"options,omitempty"` // Optional. Maximum of 100 options can be provided in each menu. SelectedOptions []AttachmentActionOption `json:"selected_options,omitempty"` // Optional. The first element of this array will be set as the pre-selected option for this menu. OptionGroups []AttachmentActionOptionGroup `json:"option_groups,omitempty"` // Optional. Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional. } // AttachmentActionOption the individual option to appear in action menu. type AttachmentActionOption struct { Text string `json:"text"` // Required. Value string `json:"value"` // Required. Description string `json:"description,omitempty"` // Optional. Up to 30 characters. } // AttachmentActionOptionGroup is a semi-hierarchal way to list available options to appear in action menu. type AttachmentActionOptionGroup struct { Text string `json:"text"` // Required. Options []AttachmentActionOption `json:"options"` // Required. } // AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction) type AttachmentActionCallback struct { Actions []AttachmentAction `json:"actions"` CallbackID string `json:"callback_id"` Team Team `json:"team"` Channel Channel `json:"channel"` User User `json:"user"` OriginalMessage Message `json:"original_message"` ActionTs string `json:"action_ts"` MessageTs string `json:"message_ts"` AttachmentID string `json:"attachment_id"` Token string `json:"token"` ResponseURL string `json:"response_url"` } // ConfirmationField are used to ask users to confirm actions type ConfirmationField struct { Title string `json:"title,omitempty"` // Optional. Text string `json:"text"` // Required. OkText string `json:"ok_text,omitempty"` // Optional. Defaults to "Okay" DismissText string `json:"dismiss_text,omitempty"` // Optional. Defaults to "Cancel" } // Attachment contains all the information for an attachment type Attachment struct { Color string `json:"color,omitempty"` Fallback string `json:"fallback"` CallbackID string `json:"callback_id,omitempty"` AuthorName string `json:"author_name,omitempty"` AuthorSubname string `json:"author_subname,omitempty"` AuthorLink string `json:"author_link,omitempty"` AuthorIcon string `json:"author_icon,omitempty"` Title string `json:"title,omitempty"` TitleLink string `json:"title_link,omitempty"` Pretext string `json:"pretext,omitempty"` Text string `json:"text"` ImageURL string `json:"image_url,omitempty"` ThumbURL string `json:"thumb_url,omitempty"` Fields []AttachmentField `json:"fields,omitempty"` Actions []AttachmentAction `json:"actions,omitempty"` MarkdownIn []string `json:"mrkdwn_in,omitempty"` Footer string `json:"footer,omitempty"` FooterIcon string `json:"footer_icon,omitempty"` Ts json.Number `json:"ts,omitempty"` } golang-github-nlopes-slack-0.1.0/backoff.go000066400000000000000000000026011311262777100205600ustar00rootroot00000000000000package slack import ( "math" "math/rand" "time" ) // This one was ripped from https://github.com/jpillora/backoff/blob/master/backoff.go // Backoff is a time.Duration counter. It starts at Min. After every // call to Duration() it is multiplied by Factor. It is capped at // Max. It returns to Min on every call to Reset(). Used in // conjunction with the time package. type backoff struct { attempts int //Factor is the multiplying factor for each increment step Factor float64 //Jitter eases contention by randomizing backoff steps Jitter bool //Min and Max are the minimum and maximum values of the counter Min, Max time.Duration } // Returns the current value of the counter and then multiplies it // Factor func (b *backoff) Duration() time.Duration { //Zero-values are nonsensical, so we use //them to apply defaults if b.Min == 0 { b.Min = 100 * time.Millisecond } if b.Max == 0 { b.Max = 10 * time.Second } if b.Factor == 0 { b.Factor = 2 } //calculate this duration dur := float64(b.Min) * math.Pow(b.Factor, float64(b.attempts)) if b.Jitter == true { dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min) } //cap! if dur > float64(b.Max) { return b.Max } //bump attempts count b.attempts++ //return as a time.Duration return time.Duration(dur) } //Resets the current value of the counter back to Min func (b *backoff) Reset() { b.attempts = 0 } golang-github-nlopes-slack-0.1.0/bots.go000066400000000000000000000016411311262777100201370ustar00rootroot00000000000000package slack import ( "errors" "net/url" ) // Bot contains information about a bot type Bot struct { ID string `json:"id"` Name string `json:"name"` Deleted bool `json:"deleted"` Icons Icons `json:"icons"` } type botResponseFull struct { Bot `json:"bot,omitempty"` // GetBotInfo SlackResponse } func botRequest(path string, values url.Values, debug bool) (*botResponseFull, error) { response := &botResponseFull{} err := post(path, values, response, debug) if err != nil { return nil, err } if !response.Ok { return nil, errors.New(response.Error) } return response, nil } // GetBotInfo will retrieve the complete bot information func (api *Client) GetBotInfo(bot string) (*Bot, error) { values := url.Values{ "token": {api.config.token}, "bot": {bot}, } response, err := botRequest("bots.info", values, api.debug) if err != nil { return nil, err } return &response.Bot, nil } golang-github-nlopes-slack-0.1.0/bots_test.go000066400000000000000000000023231311262777100211740ustar00rootroot00000000000000package slack import ( "net/http" "testing" ) func getBotInfo(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response := []byte(`{"ok": true, "bot": { "id":"B02875YLA", "deleted":false, "name":"github", "icons": { "image_36":"https:\/\/a.slack-edge.com\/2fac\/plugins\/github\/assets\/service_36.png", "image_48":"https:\/\/a.slack-edge.com\/2fac\/plugins\/github\/assets\/service_48.png", "image_72":"https:\/\/a.slack-edge.com\/2fac\/plugins\/github\/assets\/service_72.png" } }}`) rw.Write(response) } func TestGetBotInfo(t *testing.T) { http.HandleFunc("/bots.info", getBotInfo) once.Do(startServer) SLACK_API = "http://" + serverAddr + "/" api := New("testing-token") bot, err := api.GetBotInfo("B02875YLA") if err != nil { t.Errorf("Unexpected error: %s", err) return } if bot.ID != "B02875YLA" { t.Fatal("Incorrect ID") } if bot.Name != "github" { t.Fatal("Incorrect Name") } if len(bot.Icons.Image36) == 0 { t.Fatal("Missing Image36") } if len(bot.Icons.Image48) == 0 { t.Fatal("Missing Image38") } if len(bot.Icons.Image72) == 0 { t.Fatal("Missing Image72") } } golang-github-nlopes-slack-0.1.0/channels.go000066400000000000000000000166501311262777100207710ustar00rootroot00000000000000package slack import ( "errors" "net/url" "strconv" ) type channelResponseFull struct { Channel Channel `json:"channel"` Channels []Channel `json:"channels"` Purpose string `json:"purpose"` Topic string `json:"topic"` NotInChannel bool `json:"not_in_channel"` History SlackResponse } // 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"` } func channelRequest(path string, values url.Values, debug bool) (*channelResponseFull, error) { response := &channelResponseFull{} err := post(path, values, response, debug) if err != nil { return nil, err } if !response.Ok { return nil, errors.New(response.Error) } return response, nil } // ArchiveChannel archives the given channel func (api *Client) ArchiveChannel(channel string) error { values := url.Values{ "token": {api.config.token}, "channel": {channel}, } _, err := channelRequest("channels.archive", values, api.debug) if err != nil { return err } return nil } // UnarchiveChannel unarchives the given channel func (api *Client) UnarchiveChannel(channel string) error { values := url.Values{ "token": {api.config.token}, "channel": {channel}, } _, err := channelRequest("channels.unarchive", values, api.debug) if err != nil { return err } return nil } // CreateChannel creates a channel with the given name and returns a *Channel func (api *Client) CreateChannel(channel string) (*Channel, error) { values := url.Values{ "token": {api.config.token}, "name": {channel}, } response, err := channelRequest("channels.create", values, api.debug) if err != nil { return nil, err } return &response.Channel, nil } // GetChannelHistory retrieves the channel history func (api *Client) GetChannelHistory(channel string, params HistoryParameters) (*History, error) { values := url.Values{ "token": {api.config.token}, "channel": {channel}, } if params.Latest != DEFAULT_HISTORY_LATEST { values.Add("latest", params.Latest) } if params.Oldest != DEFAULT_HISTORY_OLDEST { values.Add("oldest", params.Oldest) } if params.Count != DEFAULT_HISTORY_COUNT { values.Add("count", strconv.Itoa(params.Count)) } if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { if params.Inclusive { values.Add("inclusive", "1") } else { values.Add("inclusive", "0") } } if params.Unreads != DEFAULT_HISTORY_UNREADS { if params.Unreads { values.Add("unreads", "1") } else { values.Add("unreads", "0") } } response, err := channelRequest("channels.history", values, api.debug) if err != nil { return nil, err } return &response.History, nil } // GetChannelInfo retrieves the given channel func (api *Client) GetChannelInfo(channel string) (*Channel, error) { values := url.Values{ "token": {api.config.token}, "channel": {channel}, } response, err := channelRequest("channels.info", values, api.debug) if err != nil { return nil, err } return &response.Channel, nil } // InviteUserToChannel invites a user to a given channel and returns a *Channel func (api *Client) InviteUserToChannel(channel, user string) (*Channel, error) { values := url.Values{ "token": {api.config.token}, "channel": {channel}, "user": {user}, } response, err := channelRequest("channels.invite", values, api.debug) if err != nil { return nil, err } return &response.Channel, nil } // JoinChannel joins the currently authenticated user to a channel func (api *Client) JoinChannel(channel string) (*Channel, error) { values := url.Values{ "token": {api.config.token}, "name": {channel}, } response, err := channelRequest("channels.join", values, api.debug) if err != nil { return nil, err } return &response.Channel, nil } // LeaveChannel makes the authenticated user leave the given channel func (api *Client) LeaveChannel(channel string) (bool, error) { values := url.Values{ "token": {api.config.token}, "channel": {channel}, } response, err := channelRequest("channels.leave", values, api.debug) if err != nil { return false, err } if response.NotInChannel { return response.NotInChannel, nil } return false, nil } // KickUserFromChannel kicks a user from a given channel func (api *Client) KickUserFromChannel(channel, user string) error { values := url.Values{ "token": {api.config.token}, "channel": {channel}, "user": {user}, } _, err := channelRequest("channels.kick", values, api.debug) if err != nil { return err } return nil } // GetChannels retrieves all the channels func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) { values := url.Values{ "token": {api.config.token}, } if excludeArchived { values.Add("exclude_archived", "1") } response, err := channelRequest("channels.list", values, api.debug) if err != nil { return nil, err } return response.Channels, nil } // SetChannelReadMark sets the read mark of a given channel to a specific point // Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a // timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls // (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A // timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. func (api *Client) SetChannelReadMark(channel, ts string) error { values := url.Values{ "token": {api.config.token}, "channel": {channel}, "ts": {ts}, } _, err := channelRequest("channels.mark", values, api.debug) if err != nil { return err } return nil } // RenameChannel renames a given channel func (api *Client) RenameChannel(channel, name string) (*Channel, error) { values := url.Values{ "token": {api.config.token}, "channel": {channel}, "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("channels.rename", values, api.debug) if err != nil { return nil, err } return &response.Channel, nil } // SetChannelPurpose sets the channel purpose and returns the purpose that was // successfully set func (api *Client) SetChannelPurpose(channel, purpose string) (string, error) { values := url.Values{ "token": {api.config.token}, "channel": {channel}, "purpose": {purpose}, } response, err := channelRequest("channels.setPurpose", values, api.debug) if err != nil { return "", err } return response.Purpose, nil } // SetChannelTopic sets the channel topic and returns the topic that was successfully set func (api *Client) SetChannelTopic(channel, topic string) (string, error) { values := url.Values{ "token": {api.config.token}, "channel": {channel}, "topic": {topic}, } response, err := channelRequest("channels.setTopic", values, api.debug) if err != nil { return "", err } return response.Topic, nil } // GetChannelReplies gets an entire thread (a message plus all the messages in reply to it). func (api *Client) GetChannelReplies(channel, thread_ts string) ([]Message, error) { values := url.Values{ "token": {api.config.token}, "channel": {channel}, "thread_ts": {thread_ts}, } response, err := channelRequest("channels.replies", values, api.debug) if err != nil { return nil, err } return response.History.Messages, nil } golang-github-nlopes-slack-0.1.0/chat.go000066400000000000000000000210101311262777100200770ustar00rootroot00000000000000package slack import ( "encoding/json" "errors" "net/url" "strings" ) const ( DEFAULT_MESSAGE_USERNAME = "" DEFAULT_MESSAGE_THREAD_TIMESTAMP = "" DEFAULT_MESSAGE_ASUSER = false DEFAULT_MESSAGE_PARSE = "" DEFAULT_MESSAGE_LINK_NAMES = 0 DEFAULT_MESSAGE_UNFURL_LINKS = false DEFAULT_MESSAGE_UNFURL_MEDIA = true DEFAULT_MESSAGE_ICON_URL = "" DEFAULT_MESSAGE_ICON_EMOJI = "" DEFAULT_MESSAGE_MARKDOWN = true DEFAULT_MESSAGE_ESCAPE_TEXT = true ) type chatResponseFull struct { Channel string `json:"channel"` Timestamp string `json:"ts"` Text string `json:"text"` SlackResponse } // 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"` AsUser bool `json:"as_user"` Parse string `json:"parse"` ThreadTimestamp string `json:"thread_ts"` LinkNames int `json:"link_names"` Attachments []Attachment `json:"attachments"` UnfurlLinks bool `json:"unfurl_links"` UnfurlMedia bool `json:"unfurl_media"` IconURL string `json:"icon_url"` IconEmoji string `json:"icon_emoji"` Markdown bool `json:"mrkdwn,omitempty"` EscapeText bool `json:"escape_text"` } // NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set func NewPostMessageParameters() PostMessageParameters { return PostMessageParameters{ Username: 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, } } // DeleteMessage deletes a message in a channel func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessage(channel, MsgOptionDelete(messageTimestamp)) return respChannel, respTimestamp, err } // PostMessage sends a message to 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) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessage( channel, MsgOptionText(text, params.EscapeText), MsgOptionAttachments(params.Attachments...), MsgOptionPostMessageParameters(params), ) return respChannel, respTimestamp, err } // UpdateMessage updates a message in a channel func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) { return api.SendMessage(channel, MsgOptionUpdate(timestamp), MsgOptionText(text, true)) } // SendMessage more flexible method for configuring messages. func (api *Client) SendMessage(channel string, options ...MsgOption) (string, string, string, error) { channel, values, err := ApplyMsgOptions(api.config.token, channel, options...) if err != nil { return "", "", "", err } response, err := chatRequest(channel, values, api.debug) if err != nil { return "", "", "", err } return response.Channel, response.Timestamp, response.Text, nil } // ApplyMsgOptions utility function for debugging/testing chat requests. func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) { config := sendConfig{ mode: chatPostMessage, values: url.Values{ "token": {token}, "channel": {channel}, }, } for _, opt := range options { if err := opt(&config); err != nil { return string(config.mode), config.values, err } } return string(config.mode), config.values, nil } func escapeMessage(message string) string { replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") return replacer.Replace(message) } func chatRequest(path string, values url.Values, debug bool) (*chatResponseFull, error) { response := &chatResponseFull{} err := post(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 ( chatUpdate sendMode = "chat.update" chatPostMessage sendMode = "chat.postMessage" chatDelete sendMode = "chat.delete" ) type sendConfig struct { mode sendMode values url.Values } // MsgOption option provided when sending a message. type MsgOption func(*sendConfig) error // MsgOptionPost posts a messages, this is the default. func MsgOptionPost() MsgOption { return func(config *sendConfig) error { config.mode = chatPostMessage config.values.Del("ts") return nil } } // MsgOptionUpdate updates a message based on the timestamp. func MsgOptionUpdate(timestamp string) MsgOption { return func(config *sendConfig) error { config.mode = chatUpdate config.values.Add("ts", timestamp) return nil } } // MsgOptionDelete deletes a message based on the timestamp. func MsgOptionDelete(timestamp string) MsgOption { return func(config *sendConfig) error { config.mode = chatDelete config.values.Add("ts", timestamp) return nil } } // MsgOptionAsUser whether or not to send the message as the user. func MsgOptionAsUser(b bool) MsgOption { return func(config *sendConfig) error { if b != DEFAULT_MESSAGE_ASUSER { config.values.Set("as_user", "true") } return nil } } // MsgOptionText provide the text for the message, optionally escape the provided // text. func MsgOptionText(text string, escape bool) MsgOption { return func(config *sendConfig) error { if escape { text = escapeMessage(text) } config.values.Add("text", text) return nil } } // MsgOptionAttachments provide attachments for the message. func MsgOptionAttachments(attachments ...Attachment) MsgOption { return func(config *sendConfig) error { if attachments == nil { return nil } attachments, err := json.Marshal(attachments) if err == nil { config.values.Set("attachments", string(attachments)) } return err } } // MsgOptionEnableLinkUnfurl enables link unfurling func MsgOptionEnableLinkUnfurl() MsgOption { return func(config *sendConfig) error { config.values.Set("unfurl_links", "true") return nil } } // MsgOptionDisableMediaUnfurl disables media unfurling. func MsgOptionDisableMediaUnfurl() MsgOption { return func(config *sendConfig) error { config.values.Set("unfurl_media", "false") return nil } } // MsgOptionDisableMarkdown disables markdown. func MsgOptionDisableMarkdown() MsgOption { return func(config *sendConfig) error { config.values.Set("mrkdwn", "false") 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)) } // never generates an error. MsgOptionAsUser(params.AsUser)(config) if params.Parse != DEFAULT_MESSAGE_PARSE { config.values.Set("parse", string(params.Parse)) } if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { config.values.Set("link_names", "1") } if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS { config.values.Set("unfurl_links", "true") } // I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request. // Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side. if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS { config.values.Set("unfurl_links", "false") } if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA { config.values.Set("unfurl_media", "false") } if params.IconURL != DEFAULT_MESSAGE_ICON_URL { config.values.Set("icon_url", params.IconURL) } if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI { config.values.Set("icon_emoji", params.IconEmoji) } if params.Markdown != DEFAULT_MESSAGE_MARKDOWN { config.values.Set("mrkdwn", "false") } if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP { config.values.Set("thread_ts", params.ThreadTimestamp) } return nil } } golang-github-nlopes-slack-0.1.0/comment.go000066400000000000000000000005121311262777100206260ustar00rootroot00000000000000package slack // Comment contains all the information relative to a comment type Comment struct { ID string `json:"id,omitempty"` Created JSONTime `json:"created,omitempty"` Timestamp JSONTime `json:"timestamp,omitempty"` User string `json:"user,omitempty"` Comment string `json:"comment,omitempty"` } golang-github-nlopes-slack-0.1.0/conversation.go000066400000000000000000000022301311262777100216750ustar00rootroot00000000000000package slack // Conversation is the foundation for IM and BaseGroupConversation type conversation struct { ID string `json:"id"` Created JSONTime `json:"created"` IsOpen bool `json:"is_open"` LastRead string `json:"last_read,omitempty"` Latest *Message `json:"latest,omitempty"` UnreadCount int `json:"unread_count,omitempty"` UnreadCountDisplay int `json:"unread_count_display,omitempty"` } // GroupConversation is the foundation for Group and Channel type groupConversation struct { conversation Name string `json:"name"` Creator string `json:"creator"` IsArchived bool `json:"is_archived"` Members []string `json:"members"` Topic Topic `json:"topic"` Purpose Purpose `json:"purpose"` } // Topic contains information about the topic type Topic struct { Value string `json:"value"` Creator string `json:"creator"` LastSet JSONTime `json:"last_set"` } // Purpose contains information about the purpose type Purpose struct { Value string `json:"value"` Creator string `json:"creator"` LastSet JSONTime `json:"last_set"` } golang-github-nlopes-slack-0.1.0/conversation_test.go000066400000000000000000000115731311262777100227460ustar00rootroot00000000000000package slack import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" ) // Channel var simpleChannel = `{ "id": "C024BE91L", "name": "fun", "is_channel": true, "created": 1360782804, "creator": "U024BE7LH", "is_archived": false, "is_general": false, "members": [ "U024BE7LH" ], "topic": { "value": "Fun times", "creator": "U024BE7LV", "last_set": 1369677212 }, "purpose": { "value": "This channel is for fun", "creator": "U024BE7LH", "last_set": 1360782804 }, "is_member": true, "last_read": "1401383885.000061", "unread_count": 0, "unread_count_display": 0 }` func unmarshalChannel(j string) (*Channel, error) { channel := &Channel{} if err := json.Unmarshal([]byte(j), &channel); err != nil { return nil, err } return channel, nil } func TestSimpleChannel(t *testing.T) { channel, err := unmarshalChannel(simpleChannel) assert.Nil(t, err) assertSimpleChannel(t, channel) } func assertSimpleChannel(t *testing.T, channel *Channel) { assert.NotNil(t, channel) assert.Equal(t, "C024BE91L", channel.ID) assert.Equal(t, "fun", channel.Name) assert.Equal(t, true, channel.IsChannel) assert.Equal(t, JSONTime(1360782804), channel.Created) assert.Equal(t, "U024BE7LH", channel.Creator) assert.Equal(t, false, channel.IsArchived) assert.Equal(t, false, channel.IsGeneral) assert.Equal(t, true, channel.IsMember) assert.Equal(t, "1401383885.000061", channel.LastRead) assert.Equal(t, 0, channel.UnreadCount) assert.Equal(t, 0, channel.UnreadCountDisplay) } func TestCreateSimpleChannel(t *testing.T) { channel := &Channel{} channel.ID = "C024BE91L" channel.Name = "fun" channel.IsChannel = true channel.Created = JSONTime(1360782804) channel.Creator = "U024BE7LH" channel.IsArchived = false channel.IsGeneral = false channel.IsMember = true channel.LastRead = "1401383885.000061" channel.UnreadCount = 0 channel.UnreadCountDisplay = 0 assertSimpleChannel(t, channel) } // Group var simpleGroup = `{ "id": "G024BE91L", "name": "secretplans", "is_group": true, "created": 1360782804, "creator": "U024BE7LH", "is_archived": false, "members": [ "U024BE7LH" ], "topic": { "value": "Secret plans on hold", "creator": "U024BE7LV", "last_set": 1369677212 }, "purpose": { "value": "Discuss secret plans that no-one else should know", "creator": "U024BE7LH", "last_set": 1360782804 }, "last_read": "1401383885.000061", "unread_count": 0, "unread_count_display": 0 }` func unmarshalGroup(j string) (*Group, error) { group := &Group{} if err := json.Unmarshal([]byte(j), &group); err != nil { return nil, err } return group, nil } func TestSimpleGroup(t *testing.T) { group, err := unmarshalGroup(simpleGroup) assert.Nil(t, err) assertSimpleGroup(t, group) } func assertSimpleGroup(t *testing.T, group *Group) { assert.NotNil(t, group) assert.Equal(t, "G024BE91L", group.ID) assert.Equal(t, "secretplans", group.Name) assert.Equal(t, true, group.IsGroup) assert.Equal(t, JSONTime(1360782804), group.Created) assert.Equal(t, "U024BE7LH", group.Creator) assert.Equal(t, false, group.IsArchived) assert.Equal(t, "1401383885.000061", group.LastRead) assert.Equal(t, 0, group.UnreadCount) assert.Equal(t, 0, group.UnreadCountDisplay) } func TestCreateSimpleGroup(t *testing.T) { group := &Group{} group.ID = "G024BE91L" group.Name = "secretplans" group.IsGroup = true group.Created = JSONTime(1360782804) group.Creator = "U024BE7LH" group.IsArchived = false group.LastRead = "1401383885.000061" group.UnreadCount = 0 group.UnreadCountDisplay = 0 assertSimpleGroup(t, group) } // IM var simpleIM = `{ "id": "D024BFF1M", "is_im": true, "user": "U024BE7LH", "created": 1360782804, "is_user_deleted": false, "is_open": true, "last_read": "1401383885.000061", "unread_count": 0, "unread_count_display": 0 }` func unmarshalIM(j string) (*IM, error) { im := &IM{} if err := json.Unmarshal([]byte(j), &im); err != nil { return nil, err } return im, nil } func TestSimpleIM(t *testing.T) { im, err := unmarshalIM(simpleIM) assert.Nil(t, err) assertSimpleIM(t, im) } func assertSimpleIM(t *testing.T, im *IM) { assert.NotNil(t, im) assert.Equal(t, "D024BFF1M", im.ID) assert.Equal(t, true, im.IsIM) assert.Equal(t, JSONTime(1360782804), im.Created) assert.Equal(t, false, im.IsUserDeleted) assert.Equal(t, true, im.IsOpen) assert.Equal(t, "1401383885.000061", im.LastRead) assert.Equal(t, 0, im.UnreadCount) assert.Equal(t, 0, im.UnreadCountDisplay) } func TestCreateSimpleIM(t *testing.T) { im := &IM{} im.ID = "D024BFF1M" im.IsIM = true im.Created = JSONTime(1360782804) im.IsUserDeleted = false im.IsOpen = true im.LastRead = "1401383885.000061" im.UnreadCount = 0 im.UnreadCountDisplay = 0 assertSimpleIM(t, im) } golang-github-nlopes-slack-0.1.0/dnd.go000066400000000000000000000060251311262777100177360ustar00rootroot00000000000000package slack import ( "errors" "net/url" "strconv" "strings" ) type SnoozeDebug struct { SnoozeEndDate string `json:"snooze_end_date"` } type SnoozeInfo struct { SnoozeEnabled bool `json:"snooze_enabled,omitempty"` SnoozeEndTime int `json:"snooze_endtime,omitempty"` SnoozeRemaining int `json:"snooze_remaining,omitempty"` SnoozeDebug SnoozeDebug `json:"snooze_debug,omitempty"` } type DNDStatus struct { Enabled bool `json:"dnd_enabled"` NextStartTimestamp int `json:"next_dnd_start_ts"` NextEndTimestamp int `json:"next_dnd_end_ts"` SnoozeInfo } type dndResponseFull struct { DNDStatus SlackResponse } type dndTeamInfoResponse struct { Users map[string]DNDStatus `json:"users"` SlackResponse } func dndRequest(path string, values url.Values, debug bool) (*dndResponseFull, error) { response := &dndResponseFull{} err := post(path, values, response, debug) if err != nil { return nil, err } if !response.Ok { return nil, errors.New(response.Error) } return response, nil } // EndDND ends the user's scheduled Do Not Disturb session func (api *Client) EndDND() error { values := url.Values{ "token": {api.config.token}, } response := &SlackResponse{} if err := post("dnd.endDnd", values, response, api.debug); err != nil { return err } if !response.Ok { return errors.New(response.Error) } return nil } // EndSnooze ends the current user's snooze mode func (api *Client) EndSnooze() (*DNDStatus, error) { values := url.Values{ "token": {api.config.token}, } response, err := dndRequest("dnd.endSnooze", values, api.debug) if err != nil { return nil, err } return &response.DNDStatus, nil } // GetDNDInfo provides information about a user's current Do Not Disturb settings. func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) { values := url.Values{ "token": {api.config.token}, } if user != nil { values.Set("user", *user) } response, err := dndRequest("dnd.info", values, api.debug) if err != nil { return nil, err } return &response.DNDStatus, nil } // GetDNDTeamInfo provides information about a user's current Do Not Disturb settings. func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) { values := url.Values{ "token": {api.config.token}, "users": {strings.Join(users, ",")}, } response := &dndTeamInfoResponse{} if err := post("dnd.teamInfo", values, response, api.debug); err != nil { return nil, err } if !response.Ok { return nil, errors.New(response.Error) } return response.Users, nil } // SetSnooze adjusts the snooze duration for a user's Do Not Disturb // settings. If a snooze session is not already active for the user, invoking // this method will begin one for the specified duration. func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) { values := url.Values{ "token": {api.config.token}, "num_minutes": {strconv.Itoa(minutes)}, } response, err := dndRequest("dnd.setSnooze", values, api.debug) if err != nil { return nil, err } return &response.DNDStatus, nil } golang-github-nlopes-slack-0.1.0/dnd_test.go000066400000000000000000000106041311262777100207730ustar00rootroot00000000000000package slack import ( "net/http" "reflect" "testing" ) func TestSlack_EndDND(t *testing.T) { http.HandleFunc("/dnd.endDnd", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "ok": true }`)) }) once.Do(startServer) SLACK_API = "http://" + serverAddr + "/" api := New("testing-token") err := api.EndDND() if err != nil { t.Fatalf("Unexpected error: %s", err) } } func TestSlack_EndSnooze(t *testing.T) { http.HandleFunc("/dnd.endSnooze", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "ok": true, "dnd_enabled": true, "next_dnd_start_ts": 1450418400, "next_dnd_end_ts": 1450454400, "snooze_enabled": false }`)) }) state := DNDStatus{ Enabled: true, NextStartTimestamp: 1450418400, NextEndTimestamp: 1450454400, SnoozeInfo: SnoozeInfo{SnoozeEnabled: false}, } once.Do(startServer) SLACK_API = "http://" + serverAddr + "/" api := New("testing-token") snoozeState, err := api.EndSnooze() if err != nil { t.Fatalf("Unexpected error: %s", err) } eq := reflect.DeepEqual(snoozeState, &state) if !eq { t.Errorf("got %v; want %v", snoozeState, &state) } } func TestSlack_GetDNDInfo(t *testing.T) { http.HandleFunc("/dnd.info", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "ok": true, "dnd_enabled": true, "next_dnd_start_ts": 1450416600, "next_dnd_end_ts": 1450452600, "snooze_enabled": true, "snooze_endtime": 1450416600, "snooze_remaining": 1196 }`)) }) userDNDInfo := DNDStatus{ Enabled: true, NextStartTimestamp: 1450416600, NextEndTimestamp: 1450452600, SnoozeInfo: SnoozeInfo{ SnoozeEnabled: true, SnoozeEndTime: 1450416600, SnoozeRemaining: 1196, }, } once.Do(startServer) SLACK_API = "http://" + serverAddr + "/" api := New("testing-token") userDNDInfoResponse, err := api.GetDNDInfo(nil) if err != nil { t.Fatalf("Unexpected error: %s", err) } eq := reflect.DeepEqual(userDNDInfoResponse, &userDNDInfo) if !eq { t.Errorf("got %v; want %v", userDNDInfoResponse, &userDNDInfo) } } func TestSlack_GetDNDTeamInfo(t *testing.T) { http.HandleFunc("/dnd.teamInfo", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "ok": true, "users": { "U023BECGF": { "dnd_enabled": true, "next_dnd_start_ts": 1450387800, "next_dnd_end_ts": 1450423800 }, "U058CJVAA": { "dnd_enabled": false, "next_dnd_start_ts": 1, "next_dnd_end_ts": 1 } } }`)) }) usersDNDInfo := map[string]DNDStatus{ "U023BECGF": DNDStatus{ Enabled: true, NextStartTimestamp: 1450387800, NextEndTimestamp: 1450423800, }, "U058CJVAA": DNDStatus{ Enabled: false, NextStartTimestamp: 1, NextEndTimestamp: 1, }, } once.Do(startServer) SLACK_API = "http://" + serverAddr + "/" api := New("testing-token") usersDNDInfoResponse, err := api.GetDNDTeamInfo(nil) if err != nil { t.Fatalf("Unexpected error: %s", err) } eq := reflect.DeepEqual(usersDNDInfoResponse, usersDNDInfo) if !eq { t.Errorf("got %v; want %v", usersDNDInfoResponse, usersDNDInfo) } } func TestSlack_SetSnooze(t *testing.T) { http.HandleFunc("/dnd.setSnooze", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "ok": true, "dnd_enabled": true, "snooze_endtime": 1450373897, "snooze_remaining": 60 }`)) }) snooze := DNDStatus{ Enabled: true, SnoozeInfo: SnoozeInfo{ SnoozeEndTime: 1450373897, SnoozeRemaining: 60, }, } once.Do(startServer) SLACK_API = "http://" + serverAddr + "/" api := New("testing-token") snoozeResponse, err := api.SetSnooze(60) if err != nil { t.Fatalf("Unexpected error: %s", err) } eq := reflect.DeepEqual(snoozeResponse, &snooze) if !eq { t.Errorf("got %v; want %v", snoozeResponse, &snooze) } } golang-github-nlopes-slack-0.1.0/emoji.go000066400000000000000000000010001311262777100202600ustar00rootroot00000000000000package slack import ( "errors" "net/url" ) type emojiResponseFull struct { Emoji map[string]string `json:"emoji"` SlackResponse } // GetEmoji retrieves all the emojis func (api *Client) GetEmoji() (map[string]string, error) { values := url.Values{ "token": {api.config.token}, } response := &emojiResponseFull{} err := post("emoji.list", values, response, api.debug) if err != nil { return nil, err } if !response.Ok { return nil, errors.New(response.Error) } return response.Emoji, nil } golang-github-nlopes-slack-0.1.0/emoji_test.go000066400000000000000000000017741311262777100213410ustar00rootroot00000000000000package slack import ( "net/http" "reflect" "testing" ) func getEmojiHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response := []byte(`{"ok": true, "emoji": { "bowtie": "https://my.slack.com/emoji/bowtie/46ec6f2bb0.png", "squirrel": "https://my.slack.com/emoji/squirrel/f35f40c0e0.png", "shipit": "alias:squirrel" }}`) rw.Write(response) } func TestGetEmoji(t *testing.T) { http.HandleFunc("/emoji.list", getEmojiHandler) once.Do(startServer) SLACK_API = "http://" + serverAddr + "/" api := New("testing-token") emojisResponse := map[string]string{ "bowtie": "https://my.slack.com/emoji/bowtie/46ec6f2bb0.png", "squirrel": "https://my.slack.com/emoji/squirrel/f35f40c0e0.png", "shipit": "alias:squirrel", } emojis, err := api.GetEmoji() if err != nil { t.Errorf("Unexpected error: %s", err) return } eq := reflect.DeepEqual(emojis, emojisResponse) if !eq { t.Errorf("got %v; want %v", emojis, emojisResponse) } } golang-github-nlopes-slack-0.1.0/examples/000077500000000000000000000000001311262777100204555ustar00rootroot00000000000000golang-github-nlopes-slack-0.1.0/examples/channels/000077500000000000000000000000001311262777100222505ustar00rootroot00000000000000golang-github-nlopes-slack-0.1.0/examples/channels/channels.go000066400000000000000000000004231311262777100243710ustar00rootroot00000000000000package main import ( "fmt" "github.com/nlopes/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") channels, err := api.GetChannels(false) if err != nil { fmt.Printf("%s\n", err) return } for _, channel := range channels { fmt.Println(channel.ID) } } golang-github-nlopes-slack-0.1.0/examples/files/000077500000000000000000000000001311262777100215575ustar00rootroot00000000000000golang-github-nlopes-slack-0.1.0/examples/files/example.txt000066400000000000000000000000471311262777100237540ustar00rootroot00000000000000Nan Nan Nan Nan Nan Nan Nan Nan Batman golang-github-nlopes-slack-0.1.0/examples/files/files.go000066400000000000000000000011001311262777100232000ustar00rootroot00000000000000package main import ( "fmt" "github.com/nlopes/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") params := slack.FileUploadParameters{ Title: "Batman Example", //Filetype: "txt", File: "example.txt", //Content: "Nan Nan Nan Nan Nan Nan Nan Nan Batman", } file, err := api.UploadFile(params) if err != nil { fmt.Printf("%s\n", err) return } fmt.Printf("Name: %s, URL: %s\n", file.Name, file.URL) err = api.DeleteFile(file.ID) if err != nil { fmt.Printf("%s\n", err) return } fmt.Printf("File %s deleted successfully.\n", file.Name) } golang-github-nlopes-slack-0.1.0/examples/groups/000077500000000000000000000000001311262777100217745ustar00rootroot00000000000000golang-github-nlopes-slack-0.1.0/examples/groups/groups.go000066400000000000000000000006471311262777100236510ustar00rootroot00000000000000package main import ( "fmt" "github.com/nlopes/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") // If you set debugging, it will log all requests to the console // Useful when encountering issues // api.SetDebug(true) groups, err := api.GetGroups(false) if err != nil { fmt.Printf("%s\n", err) return } for _, group := range groups { fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) } } golang-github-nlopes-slack-0.1.0/examples/messages/000077500000000000000000000000001311262777100222645ustar00rootroot00000000000000golang-github-nlopes-slack-0.1.0/examples/messages/messages.go000066400000000000000000000012611311262777100244220ustar00rootroot00000000000000package main import ( "fmt" "github.com/nlopes/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") params := slack.PostMessageParameters{} attachment := slack.Attachment{ Pretext: "some pretext", Text: "some text", // Uncomment the following part to send a field too /* Fields: []slack.AttachmentField{ slack.AttachmentField{ Title: "a", Value: "no", }, }, */ } params.Attachments = []slack.Attachment{attachment} channelID, timestamp, err := api.PostMessage("CHANNEL_ID", "Some text", params) if err != nil { fmt.Printf("%s\n", err) return } fmt.Printf("Message successfully sent to channel %s at %s", channelID, timestamp) } golang-github-nlopes-slack-0.1.0/examples/pins/000077500000000000000000000000001311262777100214265ustar00rootroot00000000000000golang-github-nlopes-slack-0.1.0/examples/pins/pins.go000066400000000000000000000053301311262777100227270ustar00rootroot00000000000000package main import ( "flag" "fmt" "github.com/nlopes/slack" ) /* WARNING: This example is destructive in the sense that it create a channel called testpinning */ func main() { var ( apiToken string debug bool ) flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token") flag.BoolVar(&debug, "debug", false, "Show JSON output") flag.Parse() api := slack.New(apiToken) if debug { api.SetDebug(true) } var ( postAsUserName string postAsUserID string postToChannelID string ) // Find the user to post as. authTest, err := api.AuthTest() if err != nil { fmt.Printf("Error getting channels: %s\n", err) return } channelName := "testpinning" // Post as the authenticated user. postAsUserName = authTest.User postAsUserID = authTest.UserID // Create a temporary channel channel, err := api.CreateChannel(channelName) if err != nil { // If the channel exists, that means we just need to unarchive it if err.Error() == "name_taken" { err = nil channels, err := api.GetChannels(false) if err != nil { fmt.Println("Could not retrieve channels") return } for _, archivedChannel := range channels { if archivedChannel.Name == channelName { if archivedChannel.IsArchived { err = api.UnarchiveChannel(archivedChannel.ID) if err != nil { fmt.Printf("Could not unarchive %s: %s\n", archivedChannel.ID, err) return } } channel = &archivedChannel break } } } if err != nil { fmt.Printf("Error setting test channel for pinning: %s\n", err) return } } postToChannelID = channel.ID fmt.Printf("Posting as %s (%s) in channel %s\n", postAsUserName, postAsUserID, postToChannelID) // Post a message. postParams := slack.PostMessageParameters{} channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams) if err != nil { fmt.Printf("Error posting message: %s\n", err) return } // Grab a reference to the message. msgRef := slack.NewRefToMessage(channelID, timestamp) // Add message pin to channel if err := api.AddPin(channelID, msgRef); err != nil { fmt.Printf("Error adding pin: %s\n", err) return } // List all of the users pins. listPins, _, err := api.ListPins(channelID) if err != nil { fmt.Printf("Error listing pins: %s\n", err) return } fmt.Printf("\n") fmt.Printf("All pins by %s...\n", authTest.User) for _, item := range listPins { fmt.Printf(" > Item type: %s\n", item.Type) } // Remove the pin. err = api.RemovePin(channelID, msgRef) if err != nil { fmt.Printf("Error remove pin: %s\n", err) return } if err = api.ArchiveChannel(channelID); err != nil { fmt.Printf("Error archiving channel: %s\n", err) return } } golang-github-nlopes-slack-0.1.0/examples/reactions/000077500000000000000000000000001311262777100224445ustar00rootroot00000000000000golang-github-nlopes-slack-0.1.0/examples/reactions/reactions.go000066400000000000000000000061551311262777100247710ustar00rootroot00000000000000package main import ( "flag" "fmt" "github.com/nlopes/slack" ) func main() { var ( apiToken string debug bool ) flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token") flag.BoolVar(&debug, "debug", false, "Show JSON output") flag.Parse() api := slack.New(apiToken) if debug { api.SetDebug(true) } var ( postAsUserName string postAsUserID string postToUserName string postToUserID string postToChannelID string ) // Find the user to post as. authTest, err := api.AuthTest() if err != nil { fmt.Printf("Error getting channels: %s\n", err) return } // Post as the authenticated user. postAsUserName = authTest.User postAsUserID = authTest.UserID // Posting to DM with self causes a conversation with slackbot. postToUserName = authTest.User postToUserID = authTest.UserID // Find the channel. _, _, chanID, err := api.OpenIMChannel(postToUserID) if err != nil { fmt.Printf("Error opening IM: %s\n", err) return } postToChannelID = chanID fmt.Printf("Posting as %s (%s) in DM with %s (%s), channel %s\n", postAsUserName, postAsUserID, postToUserName, postToUserID, postToChannelID) // Post a message. postParams := slack.PostMessageParameters{} channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams) if err != nil { fmt.Printf("Error posting message: %s\n", err) return } // Grab a reference to the message. msgRef := slack.NewRefToMessage(channelID, timestamp) // React with :+1: if err := api.AddReaction("+1", msgRef); err != nil { fmt.Printf("Error adding reaction: %s\n", err) return } // React with :-1: if err := api.AddReaction("cry", msgRef); err != nil { fmt.Printf("Error adding reaction: %s\n", err) return } // Get all reactions on the message. msgReactions, err := api.GetReactions(msgRef, slack.NewGetReactionsParameters()) if err != nil { fmt.Printf("Error getting reactions: %s\n", err) return } fmt.Printf("\n") fmt.Printf("%d reactions to message...\n", len(msgReactions)) for _, r := range msgReactions { fmt.Printf(" %d users say %s\n", r.Count, r.Name) } // List all of the users reactions. listReactions, _, err := api.ListReactions(slack.NewListReactionsParameters()) if err != nil { fmt.Printf("Error listing reactions: %s\n", err) return } fmt.Printf("\n") fmt.Printf("All reactions by %s...\n", authTest.User) for _, item := range listReactions { fmt.Printf("%d on a %s...\n", len(item.Reactions), item.Type) for _, r := range item.Reactions { fmt.Printf(" %s (along with %d others)\n", r.Name, r.Count-1) } } // Remove the :cry: reaction. err = api.RemoveReaction("cry", msgRef) if err != nil { fmt.Printf("Error remove reaction: %s\n", err) return } // Get all reactions on the message. msgReactions, err = api.GetReactions(msgRef, slack.NewGetReactionsParameters()) if err != nil { fmt.Printf("Error getting reactions: %s\n", err) return } fmt.Printf("\n") fmt.Printf("%d reactions to message after removing cry...\n", len(msgReactions)) for _, r := range msgReactions { fmt.Printf(" %d users say %s\n", r.Count, r.Name) } } golang-github-nlopes-slack-0.1.0/examples/stars/000077500000000000000000000000001311262777100216115ustar00rootroot00000000000000golang-github-nlopes-slack-0.1.0/examples/stars/stars.go000066400000000000000000000016341311262777100233000ustar00rootroot00000000000000package main import ( "flag" "fmt" "github.com/nlopes/slack" ) func main() { var ( apiToken string debug bool ) flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token") flag.BoolVar(&debug, "debug", false, "Show JSON output") flag.Parse() api := slack.New(apiToken) if debug { api.SetDebug(true) } // Get all stars for the usr. params := slack.NewStarsParameters() starredItems, _, err := api.GetStarred(params) if err != nil { fmt.Printf("Error getting stars: %s\n", err) return } for _, s := range starredItems { var desc string switch s.Type { case slack.TYPE_MESSAGE: desc = s.Message.Text case slack.TYPE_FILE: desc = s.File.Name case slack.TYPE_FILE_COMMENT: desc = s.File.Name + " - " + s.Comment.Comment case slack.TYPE_CHANNEL, slack.TYPE_IM, slack.TYPE_GROUP: desc = s.Channel } fmt.Printf("Starred %s: %s\n", s.Type, desc) } } golang-github-nlopes-slack-0.1.0/examples/team/000077500000000000000000000000001311262777100214035ustar00rootroot00000000000000golang-github-nlopes-slack-0.1.0/examples/team/team.go000066400000000000000000000010171311262777100226570ustar00rootroot00000000000000package main import ( "fmt" "github.com/nlopes/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") //Example for single user billingActive, err := api.GetBillableInfo("U023BECGF") if err != nil { fmt.Printf("%s\n", err) return } fmt.Printf("ID: U023BECGF, BillingActive: %v\n\n\n", billingActive["U023BECGF"]) //Example for team billingActiveForTeam, err := api.GetBillableInfoForTeam() for id, value := range billingActiveForTeam { fmt.Printf("ID: %v, BillingActive: %v\n", id, value) } } golang-github-nlopes-slack-0.1.0/examples/users/000077500000000000000000000000001311262777100216165ustar00rootroot00000000000000golang-github-nlopes-slack-0.1.0/examples/users/users.go000066400000000000000000000004711311262777100233100ustar00rootroot00000000000000package main import ( "fmt" "github.com/nlopes/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") user, err := api.GetUserInfo("U023BECGF") if err != nil { fmt.Printf("%s\n", err) return } fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email) } golang-github-nlopes-slack-0.1.0/examples/websocket/000077500000000000000000000000001311262777100224435ustar00rootroot00000000000000golang-github-nlopes-slack-0.1.0/examples/websocket/websocket.go000066400000000000000000000021721311262777100247620ustar00rootroot00000000000000package main import ( "fmt" "log" "os" "github.com/nlopes/slack" ) func main() { api := slack.New("YOUR TOKEN HERE") logger := log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags) slack.SetLogger(logger) api.SetDebug(true) rtm := api.NewRTM() go rtm.ManageConnection() for msg := range rtm.IncomingEvents { fmt.Print("Event Received: ") switch ev := msg.Data.(type) { case *slack.HelloEvent: // Ignore hello case *slack.ConnectedEvent: fmt.Println("Infos:", ev.Info) fmt.Println("Connection counter:", ev.ConnectionCount) // Replace #general with your Channel ID rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", "#general")) case *slack.MessageEvent: fmt.Printf("Message: %v\n", ev) case *slack.PresenceChangeEvent: fmt.Printf("Presence Change: %v\n", ev) case *slack.LatencyReport: fmt.Printf("Current latency: %v\n", ev.Value) case *slack.RTMError: fmt.Printf("Error: %s\n", ev.Error()) case *slack.InvalidAuthEvent: fmt.Printf("Invalid credentials") return default: // Ignore other events.. // fmt.Printf("Unexpected: %v\n", msg.Data) } } } golang-github-nlopes-slack-0.1.0/files.go000066400000000000000000000205461311262777100202770ustar00rootroot00000000000000package slack import ( "errors" "io" "net/url" "strconv" "strings" ) const ( // Add here the defaults in the siten DEFAULT_FILES_USER = "" DEFAULT_FILES_CHANNEL = "" DEFAULT_FILES_TS_FROM = 0 DEFAULT_FILES_TS_TO = -1 DEFAULT_FILES_TYPES = "all" DEFAULT_FILES_COUNT = 100 DEFAULT_FILES_PAGE = 1 ) // File contains all the information for a file type File struct { ID string `json:"id"` Created JSONTime `json:"created"` Timestamp JSONTime `json:"timestamp"` Name string `json:"name"` Title string `json:"title"` Mimetype string `json:"mimetype"` ImageExifRotation int `json:"image_exif_rotation"` Filetype string `json:"filetype"` PrettyType string `json:"pretty_type"` User string `json:"user"` Mode string `json:"mode"` Editable bool `json:"editable"` IsExternal bool `json:"is_external"` ExternalType string `json:"external_type"` Size int `json:"size"` URL string `json:"url"` // Deprecated - never set URLDownload string `json:"url_download"` // Deprecated - never set URLPrivate string `json:"url_private"` URLPrivateDownload string `json:"url_private_download"` OriginalH int `json:"original_h"` OriginalW int `json:"original_w"` Thumb64 string `json:"thumb_64"` Thumb80 string `json:"thumb_80"` Thumb160 string `json:"thumb_160"` Thumb360 string `json:"thumb_360"` Thumb360Gif string `json:"thumb_360_gif"` Thumb360W int `json:"thumb_360_w"` Thumb360H int `json:"thumb_360_h"` Thumb480 string `json:"thumb_480"` Thumb480W int `json:"thumb_480_w"` Thumb480H int `json:"thumb_480_h"` Thumb720 string `json:"thumb_720"` Thumb720W int `json:"thumb_720_w"` Thumb720H int `json:"thumb_720_h"` Thumb960 string `json:"thumb_960"` Thumb960W int `json:"thumb_960_w"` Thumb960H int `json:"thumb_960_h"` Thumb1024 string `json:"thumb_1024"` Thumb1024W int `json:"thumb_1024_w"` Thumb1024H int `json:"thumb_1024_h"` Permalink string `json:"permalink"` PermalinkPublic string `json:"permalink_public"` EditLink string `json:"edit_link"` Preview string `json:"preview"` PreviewHighlight string `json:"preview_highlight"` Lines int `json:"lines"` LinesMore int `json:"lines_more"` IsPublic bool `json:"is_public"` PublicURLShared bool `json:"public_url_shared"` Channels []string `json:"channels"` Groups []string `json:"groups"` IMs []string `json:"ims"` InitialComment Comment `json:"initial_comment"` CommentsCount int `json:"comments_count"` NumStars int `json:"num_stars"` IsStarred bool `json:"is_starred"` } // FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request. // // There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large, // or provide a local file path in File to upload it from your filesystem. type FileUploadParameters struct { File string Content string Reader io.Reader Filetype string Filename string Title string InitialComment string Channels []string } // GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request type GetFilesParameters struct { User string Channel string TimestampFrom JSONTime TimestampTo JSONTime Types string Count int Page int } type fileResponseFull struct { File `json:"file"` Paging `json:"paging"` Comments []Comment `json:"comments"` Files []File `json:"files"` SlackResponse } // NewGetFilesParameters provides an instance of GetFilesParameters with all the sane default values set func NewGetFilesParameters() GetFilesParameters { return GetFilesParameters{ User: DEFAULT_FILES_USER, Channel: DEFAULT_FILES_CHANNEL, TimestampFrom: DEFAULT_FILES_TS_FROM, TimestampTo: DEFAULT_FILES_TS_TO, Types: DEFAULT_FILES_TYPES, Count: DEFAULT_FILES_COUNT, Page: DEFAULT_FILES_PAGE, } } func fileRequest(path string, values url.Values, debug bool) (*fileResponseFull, error) { response := &fileResponseFull{} err := post(path, values, response, debug) if err != nil { return nil, err } if !response.Ok { return nil, errors.New(response.Error) } return response, nil } // GetFileInfo retrieves a file and related comments func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment, *Paging, error) { values := url.Values{ "token": {api.config.token}, "file": {fileID}, "count": {strconv.Itoa(count)}, "page": {strconv.Itoa(page)}, } response, err := fileRequest("files.info", values, api.debug) if err != nil { return nil, nil, nil, err } return &response.File, response.Comments, &response.Paging, nil } // GetFiles retrieves all files according to the parameters given func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) { values := url.Values{ "token": {api.config.token}, } if params.User != DEFAULT_FILES_USER { values.Add("user", params.User) } if params.Channel != DEFAULT_FILES_CHANNEL { values.Add("channel", params.Channel) } if params.TimestampFrom != DEFAULT_FILES_TS_FROM { values.Add("ts_from", strconv.FormatInt(int64(params.TimestampFrom), 10)) } if params.TimestampTo != DEFAULT_FILES_TS_TO { values.Add("ts_to", strconv.FormatInt(int64(params.TimestampTo), 10)) } if params.Types != DEFAULT_FILES_TYPES { values.Add("types", params.Types) } if params.Count != DEFAULT_FILES_COUNT { values.Add("count", strconv.Itoa(params.Count)) } if params.Page != DEFAULT_FILES_PAGE { values.Add("page", strconv.Itoa(params.Page)) } response, err := fileRequest("files.list", values, api.debug) if err != nil { return nil, nil, err } return response.Files, &response.Paging, nil } // UploadFile uploads a file func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More // investigation needed, but for now this will do. _, err = api.AuthTest() if err != nil { return nil, err } response := &fileResponseFull{} values := url.Values{ "token": {api.config.token}, } if params.Filetype != "" { values.Add("filetype", params.Filetype) } if params.Filename != "" { values.Add("filename", params.Filename) } if params.Title != "" { values.Add("title", params.Title) } if params.InitialComment != "" { values.Add("initial_comment", params.InitialComment) } if len(params.Channels) != 0 { values.Add("channels", strings.Join(params.Channels, ",")) } if params.Content != "" { values.Add("content", params.Content) err = post("files.upload", values, response, api.debug) } else if params.File != "" { err = postLocalWithMultipartResponse("files.upload", params.File, "file", values, response, api.debug) } else if params.Reader != nil { err = postWithMultipartResponse("files.upload", params.Filename, "file", values, params.Reader, response, api.debug) } if err != nil { return nil, err } if !response.Ok { return nil, errors.New(response.Error) } return &response.File, nil } // DeleteFile deletes a file func (api *Client) DeleteFile(fileID string) error { values := url.Values{ "token": {api.config.token}, "file": {fileID}, } _, err := fileRequest("files.delete", values, api.debug) if err != nil { return err } return nil } // RevokeFilePublicURL disables public/external sharing for a file func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) { values := url.Values{ "token": {api.config.token}, "file": {fileID}, } response, err := fileRequest("files.revokePublicURL", values, api.debug) if err != nil { return nil, err } return &response.File, nil } // ShareFilePublicURL enabled public/external sharing for a file func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging, error) { values := url.Values{ "token": {api.config.token}, "file": {fileID}, } response, err := fileRequest("files.sharedPublicURL", values, api.debug) if err != nil { return nil, nil, nil, err } return &response.File, response.Comments, &response.Paging, nil } golang-github-nlopes-slack-0.1.0/groups.go000066400000000000000000000174431311262777100205160ustar00rootroot00000000000000package slack import ( "errors" "net/url" "strconv" ) // Group contains all the information for a group type Group struct { groupConversation IsGroup bool `json:"is_group"` } type groupResponseFull struct { Group Group `json:"group"` Groups []Group `json:"groups"` Purpose string `json:"purpose"` Topic string `json:"topic"` NotInGroup bool `json:"not_in_group"` NoOp bool `json:"no_op"` AlreadyClosed bool `json:"already_closed"` AlreadyOpen bool `json:"already_open"` AlreadyInGroup bool `json:"already_in_group"` Channel Channel `json:"channel"` History SlackResponse } func groupRequest(path string, values url.Values, debug bool) (*groupResponseFull, error) { response := &groupResponseFull{} err := post(path, values, response, debug) if err != nil { return nil, err } if !response.Ok { return nil, errors.New(response.Error) } return response, nil } // ArchiveGroup archives a private group func (api *Client) ArchiveGroup(group string) error { values := url.Values{ "token": {api.config.token}, "channel": {group}, } _, err := groupRequest("groups.archive", values, api.debug) if err != nil { return err } return nil } // UnarchiveGroup unarchives a private group func (api *Client) UnarchiveGroup(group string) error { values := url.Values{ "token": {api.config.token}, "channel": {group}, } _, err := groupRequest("groups.unarchive", values, api.debug) if err != nil { return err } return nil } // CreateGroup creates a private group func (api *Client) CreateGroup(group string) (*Group, error) { values := url.Values{ "token": {api.config.token}, "name": {group}, } response, err := groupRequest("groups.create", values, api.debug) if err != nil { return nil, err } return &response.Group, nil } // CreateChildGroup creates a new private group archiving the old one // This method takes an existing private group and performs the following steps: // 1. Renames the existing group (from "example" to "example-archived"). // 2. Archives the existing group. // 3. Creates a new group with the name of the existing group. // 4. Adds all members of the existing group to the new group. func (api *Client) CreateChildGroup(group string) (*Group, error) { values := url.Values{ "token": {api.config.token}, "channel": {group}, } response, err := groupRequest("groups.createChild", values, api.debug) if err != nil { return nil, err } return &response.Group, nil } // CloseGroup closes a private group func (api *Client) CloseGroup(group string) (bool, bool, error) { values := url.Values{ "token": {api.config.token}, "channel": {group}, } response, err := imRequest("groups.close", values, api.debug) if err != nil { return false, false, err } return response.NoOp, response.AlreadyClosed, nil } // GetGroupHistory fetches all the history for a private group func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) { values := url.Values{ "token": {api.config.token}, "channel": {group}, } if params.Latest != DEFAULT_HISTORY_LATEST { values.Add("latest", params.Latest) } if params.Oldest != DEFAULT_HISTORY_OLDEST { values.Add("oldest", params.Oldest) } if params.Count != DEFAULT_HISTORY_COUNT { values.Add("count", strconv.Itoa(params.Count)) } if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { if params.Inclusive { values.Add("inclusive", "1") } else { values.Add("inclusive", "0") } } if params.Unreads != DEFAULT_HISTORY_UNREADS { if params.Unreads { values.Add("unreads", "1") } else { values.Add("unreads", "0") } } response, err := groupRequest("groups.history", values, api.debug) if err != nil { return nil, err } return &response.History, nil } // InviteUserToGroup invites a specific user to a private group func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) { values := url.Values{ "token": {api.config.token}, "channel": {group}, "user": {user}, } response, err := groupRequest("groups.invite", values, api.debug) if err != nil { return nil, false, err } return &response.Group, response.AlreadyInGroup, nil } // LeaveGroup makes authenticated user leave the group func (api *Client) LeaveGroup(group string) error { values := url.Values{ "token": {api.config.token}, "channel": {group}, } _, err := groupRequest("groups.leave", values, api.debug) if err != nil { return err } return nil } // KickUserFromGroup kicks a user from a group func (api *Client) KickUserFromGroup(group, user string) error { values := url.Values{ "token": {api.config.token}, "channel": {group}, "user": {user}, } _, err := groupRequest("groups.kick", values, api.debug) if err != nil { return err } return nil } // GetGroups retrieves all groups func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) { values := url.Values{ "token": {api.config.token}, } if excludeArchived { values.Add("exclude_archived", "1") } response, err := groupRequest("groups.list", values, api.debug) if err != nil { return nil, err } return response.Groups, nil } // GetGroupInfo retrieves the given group func (api *Client) GetGroupInfo(group string) (*Group, error) { values := url.Values{ "token": {api.config.token}, "channel": {group}, } response, err := groupRequest("groups.info", values, api.debug) if err != nil { return nil, err } return &response.Group, nil } // SetGroupReadMark sets the read mark on a private group // Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a // timer before making the call. In this way, any further updates needed during the timeout will not generate extra // calls (just one per channel). This is useful for when reading scroll-back history, or following a busy live // channel. A timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. func (api *Client) SetGroupReadMark(group, ts string) error { values := url.Values{ "token": {api.config.token}, "channel": {group}, "ts": {ts}, } _, err := groupRequest("groups.mark", values, api.debug) if err != nil { return err } return nil } // OpenGroup opens a private group func (api *Client) OpenGroup(group string) (bool, bool, error) { values := url.Values{ "token": {api.config.token}, "channel": {group}, } response, err := groupRequest("groups.open", values, api.debug) if err != nil { return false, false, err } return response.NoOp, response.AlreadyOpen, nil } // RenameGroup renames a group // XXX: They return a channel, not a group. What is this crap? :( // Inconsistent api it seems. func (api *Client) RenameGroup(group, name string) (*Channel, error) { values := url.Values{ "token": {api.config.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("groups.rename", values, api.debug) if err != nil { return nil, err } return &response.Channel, nil } // SetGroupPurpose sets the group purpose func (api *Client) SetGroupPurpose(group, purpose string) (string, error) { values := url.Values{ "token": {api.config.token}, "channel": {group}, "purpose": {purpose}, } response, err := groupRequest("groups.setPurpose", values, api.debug) if err != nil { return "", err } return response.Purpose, nil } // SetGroupTopic sets the group topic func (api *Client) SetGroupTopic(group, topic string) (string, error) { values := url.Values{ "token": {api.config.token}, "channel": {group}, "topic": {topic}, } response, err := groupRequest("groups.setTopic", values, api.debug) if err != nil { return "", err } return response.Topic, nil } golang-github-nlopes-slack-0.1.0/history.go000066400000000000000000000020161311262777100206660ustar00rootroot00000000000000package slack const ( DEFAULT_HISTORY_LATEST = "" DEFAULT_HISTORY_OLDEST = "0" DEFAULT_HISTORY_COUNT = 100 DEFAULT_HISTORY_INCLUSIVE = false DEFAULT_HISTORY_UNREADS = false ) // HistoryParameters contains all the necessary information to help in the retrieval of history for Channels/Groups/DMs type HistoryParameters struct { Latest string Oldest string Count int Inclusive bool Unreads bool } // History contains message history information needed to navigate a Channel / Group / DM history type History struct { Latest string `json:"latest"` Messages []Message `json:"messages"` HasMore bool `json:"has_more"` } // NewHistoryParameters provides an instance of HistoryParameters with all the sane default values set func NewHistoryParameters() HistoryParameters { return HistoryParameters{ Latest: DEFAULT_HISTORY_LATEST, Oldest: DEFAULT_HISTORY_OLDEST, Count: DEFAULT_HISTORY_COUNT, Inclusive: DEFAULT_HISTORY_INCLUSIVE, Unreads: DEFAULT_HISTORY_UNREADS, } } golang-github-nlopes-slack-0.1.0/im.go000066400000000000000000000063431311262777100176010ustar00rootroot00000000000000package slack import ( "errors" "net/url" "strconv" ) type imChannel struct { ID string `json:"id"` } type imResponseFull struct { NoOp bool `json:"no_op"` AlreadyClosed bool `json:"already_closed"` AlreadyOpen bool `json:"already_open"` Channel imChannel `json:"channel"` IMs []IM `json:"ims"` History SlackResponse } // IM contains information related to the Direct Message channel type IM struct { conversation IsIM bool `json:"is_im"` User string `json:"user"` IsUserDeleted bool `json:"is_user_deleted"` } func imRequest(path string, values url.Values, debug bool) (*imResponseFull, error) { response := &imResponseFull{} err := post(path, values, response, debug) if err != nil { return nil, err } if !response.Ok { return nil, errors.New(response.Error) } return response, nil } // CloseIMChannel closes the direct message channel func (api *Client) CloseIMChannel(channel string) (bool, bool, error) { values := url.Values{ "token": {api.config.token}, "channel": {channel}, } response, err := imRequest("im.close", values, api.debug) if err != nil { return false, false, err } return response.NoOp, response.AlreadyClosed, nil } // OpenIMChannel opens a direct message channel to the user provided as argument // Returns some status and the channel ID func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) { values := url.Values{ "token": {api.config.token}, "user": {user}, } response, err := imRequest("im.open", values, api.debug) if err != nil { return false, false, "", err } return response.NoOp, response.AlreadyOpen, response.Channel.ID, nil } // MarkIMChannel sets the read mark of a direct message channel to a specific point func (api *Client) MarkIMChannel(channel, ts string) (err error) { values := url.Values{ "token": {api.config.token}, "channel": {channel}, "ts": {ts}, } _, err = imRequest("im.mark", values, api.debug) if err != nil { return err } return } // GetIMHistory retrieves the direct message channel history func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*History, error) { values := url.Values{ "token": {api.config.token}, "channel": {channel}, } if params.Latest != DEFAULT_HISTORY_LATEST { values.Add("latest", params.Latest) } if params.Oldest != DEFAULT_HISTORY_OLDEST { values.Add("oldest", params.Oldest) } if params.Count != DEFAULT_HISTORY_COUNT { values.Add("count", strconv.Itoa(params.Count)) } if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { if params.Inclusive { values.Add("inclusive", "1") } else { values.Add("inclusive", "0") } } if params.Unreads != DEFAULT_HISTORY_UNREADS { if params.Unreads { values.Add("unreads", "1") } else { values.Add("unreads", "0") } } response, err := imRequest("im.history", values, api.debug) if err != nil { return nil, err } return &response.History, nil } // GetIMChannels returns the list of direct message channels func (api *Client) GetIMChannels() ([]IM, error) { values := url.Values{ "token": {api.config.token}, } response, err := imRequest("im.list", values, api.debug) if err != nil { return nil, err } return response.IMs, nil } golang-github-nlopes-slack-0.1.0/info.go000066400000000000000000000132031311262777100201200ustar00rootroot00000000000000package slack import ( "fmt" "time" ) // UserPrefs needs to be implemented type UserPrefs struct { // "highlight_words":"", // "user_colors":"", // "color_names_in_list":true, // "growls_enabled":true, // "tz":"Europe\/London", // "push_dm_alert":true, // "push_mention_alert":true, // "push_everything":true, // "push_idle_wait":2, // "push_sound":"b2.mp3", // "push_loud_channels":"", // "push_mention_channels":"", // "push_loud_channels_set":"", // "email_alerts":"instant", // "email_alerts_sleep_until":0, // "email_misc":false, // "email_weekly":true, // "welcome_message_hidden":false, // "all_channels_loud":true, // "loud_channels":"", // "never_channels":"", // "loud_channels_set":"", // "show_member_presence":true, // "search_sort":"timestamp", // "expand_inline_imgs":true, // "expand_internal_inline_imgs":true, // "expand_snippets":false, // "posts_formatting_guide":true, // "seen_welcome_2":true, // "seen_ssb_prompt":false, // "search_only_my_channels":false, // "emoji_mode":"default", // "has_invited":true, // "has_uploaded":false, // "has_created_channel":true, // "search_exclude_channels":"", // "messages_theme":"default", // "webapp_spellcheck":true, // "no_joined_overlays":false, // "no_created_overlays":true, // "dropbox_enabled":false, // "seen_user_menu_tip_card":true, // "seen_team_menu_tip_card":true, // "seen_channel_menu_tip_card":true, // "seen_message_input_tip_card":true, // "seen_channels_tip_card":true, // "seen_domain_invite_reminder":false, // "seen_member_invite_reminder":false, // "seen_flexpane_tip_card":true, // "seen_search_input_tip_card":true, // "mute_sounds":false, // "arrow_history":false, // "tab_ui_return_selects":true, // "obey_inline_img_limit":true, // "new_msg_snd":"knock_brush.mp3", // "collapsible":false, // "collapsible_by_click":true, // "require_at":false, // "mac_ssb_bounce":"", // "mac_ssb_bullet":true, // "win_ssb_bullet":true, // "expand_non_media_attachments":true, // "show_typing":true, // "pagekeys_handled":true, // "last_snippet_type":"", // "display_real_names_override":0, // "time24":false, // "enter_is_special_in_tbt":false, // "graphic_emoticons":false, // "convert_emoticons":true, // "autoplay_chat_sounds":true, // "ss_emojis":true, // "sidebar_behavior":"", // "mark_msgs_read_immediately":true, // "start_scroll_at_oldest":true, // "snippet_editor_wrap_long_lines":false, // "ls_disabled":false, // "sidebar_theme":"default", // "sidebar_theme_custom_values":"", // "f_key_search":false, // "k_key_omnibox":true, // "speak_growls":false, // "mac_speak_voice":"com.apple.speech.synthesis.voice.Alex", // "mac_speak_speed":250, // "comma_key_prefs":false, // "at_channel_suppressed_channels":"", // "push_at_channel_suppressed_channels":"", // "prompted_for_email_disabling":false, // "full_text_extracts":false, // "no_text_in_notifications":false, // "muted_channels":"", // "no_macssb1_banner":false, // "privacy_policy_seen":true, // "search_exclude_bots":false, // "fuzzy_matching":false } // UserDetails contains user details coming in the initial response from StartRTM type UserDetails struct { ID string `json:"id"` Name string `json:"name"` Created JSONTime `json:"created"` ManualPresence string `json:"manual_presence"` Prefs UserPrefs `json:"prefs"` } // JSONTime exists so that we can have a String method converting the date type JSONTime int64 // String converts the unix timestamp into a string func (t JSONTime) String() string { tm := t.Time() return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) } // Time returns a `time.Time` representation of this value. func (t JSONTime) Time() time.Time { return time.Unix(int64(t), 0) } // Team contains details about a team type Team struct { ID string `json:"id"` Name string `json:"name"` Domain string `json:"domain"` } // Icons XXX: needs further investigation type Icons struct { Image36 string `json:"image_36,omitempty"` Image48 string `json:"image_48,omitempty"` Image72 string `json:"image_72,omitempty"` } // Info contains various details about Users, Channels, Bots and the authenticated user. // It is returned by StartRTM or included in the "ConnectedEvent" RTM event. type Info struct { URL string `json:"url,omitempty"` User *UserDetails `json:"self,omitempty"` Team *Team `json:"team,omitempty"` Users []User `json:"users,omitempty"` Channels []Channel `json:"channels,omitempty"` Groups []Group `json:"groups,omitempty"` Bots []Bot `json:"bots,omitempty"` IMs []IM `json:"ims,omitempty"` } type infoResponseFull struct { Info WebResponse } // GetBotByID returns a bot given a bot id func (info Info) GetBotByID(botID string) *Bot { for _, bot := range info.Bots { if bot.ID == botID { return &bot } } return nil } // GetUserByID returns a user given a user id func (info Info) GetUserByID(userID string) *User { for _, user := range info.Users { if user.ID == userID { return &user } } return nil } // GetChannelByID returns a channel given a channel id func (info Info) GetChannelByID(channelID string) *Channel { for _, channel := range info.Channels { if channel.ID == channelID { return &channel } } return nil } // GetGroupByID returns a group given a group id func (info Info) GetGroupByID(groupID string) *Group { for _, group := range info.Groups { if group.ID == groupID { return &group } } return nil } // GetIMByID returns an IM given an IM id func (info Info) GetIMByID(imID string) *IM { for _, im := range info.IMs { if im.ID == imID { return &im } } return nil } golang-github-nlopes-slack-0.1.0/item.go000066400000000000000000000043321311262777100201260ustar00rootroot00000000000000package slack const ( TYPE_MESSAGE = "message" TYPE_FILE = "file" TYPE_FILE_COMMENT = "file_comment" TYPE_CHANNEL = "channel" TYPE_IM = "im" TYPE_GROUP = "group" ) // Item is any type of slack message - message, file, or file comment. type Item struct { Type string `json:"type"` Channel string `json:"channel,omitempty"` Message *Message `json:"message,omitempty"` File *File `json:"file,omitempty"` Comment *Comment `json:"comment,omitempty"` Timestamp string `json:"ts,omitempty"` } // NewMessageItem turns a message on a channel into a typed message struct. func NewMessageItem(ch string, m *Message) Item { return Item{Type: TYPE_MESSAGE, Channel: ch, Message: m} } // NewFileItem turns a file into a typed file struct. func NewFileItem(f *File) Item { return Item{Type: TYPE_FILE, File: f} } // NewFileCommentItem turns a file and comment into a typed file_comment struct. func NewFileCommentItem(f *File, c *Comment) Item { return Item{Type: TYPE_FILE_COMMENT, File: f, Comment: c} } // NewChannelItem turns a channel id into a typed channel struct. func NewChannelItem(ch string) Item { return Item{Type: TYPE_CHANNEL, Channel: ch} } // NewIMItem turns a channel id into a typed im struct. func NewIMItem(ch string) Item { return Item{Type: TYPE_IM, Channel: ch} } // NewGroupItem turns a channel id into a typed group struct. func NewGroupItem(ch string) Item { return Item{Type: TYPE_GROUP, Channel: ch} } // ItemRef is a reference to a message of any type. One of FileID, // CommentId, or the combination of ChannelId and Timestamp must be // specified. type ItemRef struct { Channel string `json:"channel"` Timestamp string `json:"timestamp"` File string `json:"file"` Comment string `json:"file_comment"` } // NewRefToMessage initializes a reference to to a message. func NewRefToMessage(channel, timestamp string) ItemRef { return ItemRef{Channel: channel, Timestamp: timestamp} } // NewRefToFile initializes a reference to a file. func NewRefToFile(file string) ItemRef { return ItemRef{File: file} } // NewRefToComment initializes a reference to a file comment. func NewRefToComment(comment string) ItemRef { return ItemRef{Comment: comment} } golang-github-nlopes-slack-0.1.0/item_test.go000066400000000000000000000061271311262777100211710ustar00rootroot00000000000000package slack import "testing" func TestNewMessageItem(t *testing.T) { c := "C1" m := &Message{} mi := NewMessageItem(c, m) if mi.Type != TYPE_MESSAGE { t.Errorf("want Type %s, got %s", mi.Type, TYPE_MESSAGE) } if mi.Channel != c { t.Errorf("got Channel %s, want %s", mi.Channel, c) } if mi.Message != m { t.Errorf("got Message %v, want %v", mi.Message, m) } } func TestNewFileItem(t *testing.T) { f := &File{} fi := NewFileItem(f) if fi.Type != TYPE_FILE { t.Errorf("got Type %s, want %s", fi.Type, TYPE_FILE) } if fi.File != f { t.Errorf("got File %v, want %v", fi.File, f) } } func TestNewFileCommentItem(t *testing.T) { f := &File{} c := &Comment{} fci := NewFileCommentItem(f, c) if fci.Type != TYPE_FILE_COMMENT { t.Errorf("got Type %s, want %s", fci.Type, TYPE_FILE_COMMENT) } if fci.File != f { t.Errorf("got File %v, want %v", fci.File, f) } if fci.Comment != c { t.Errorf("got Comment %v, want %v", fci.Comment, c) } } func TestNewChannelItem(t *testing.T) { c := "C1" ci := NewChannelItem(c) if ci.Type != TYPE_CHANNEL { t.Errorf("got Type %s, want %s", ci.Type, TYPE_CHANNEL) } if ci.Channel != "C1" { t.Errorf("got Channel %v, want %v", ci.Channel, "C1") } } func TestNewIMItem(t *testing.T) { c := "D1" ci := NewIMItem(c) if ci.Type != TYPE_IM { t.Errorf("got Type %s, want %s", ci.Type, TYPE_IM) } if ci.Channel != "D1" { t.Errorf("got Channel %v, want %v", ci.Channel, "D1") } } func TestNewGroupItem(t *testing.T) { c := "G1" ci := NewGroupItem(c) if ci.Type != TYPE_GROUP { t.Errorf("got Type %s, want %s", ci.Type, TYPE_GROUP) } if ci.Channel != "G1" { t.Errorf("got Channel %v, want %v", ci.Channel, "G1") } } func TestNewRefToMessage(t *testing.T) { ref := NewRefToMessage("chan", "ts") if got, want := ref.Channel, "chan"; got != want { t.Errorf("Channel got %s, want %s", got, want) } if got, want := ref.Timestamp, "ts"; got != want { t.Errorf("Timestamp got %s, want %s", got, want) } if got, want := ref.File, ""; got != want { t.Errorf("File got %s, want %s", got, want) } if got, want := ref.Comment, ""; got != want { t.Errorf("Comment got %s, want %s", got, want) } } func TestNewRefToFile(t *testing.T) { ref := NewRefToFile("file") if got, want := ref.Channel, ""; got != want { t.Errorf("Channel got %s, want %s", got, want) } if got, want := ref.Timestamp, ""; got != want { t.Errorf("Timestamp got %s, want %s", got, want) } if got, want := ref.File, "file"; got != want { t.Errorf("File got %s, want %s", got, want) } if got, want := ref.Comment, ""; got != want { t.Errorf("Comment got %s, want %s", got, want) } } func TestNewRefToComment(t *testing.T) { ref := NewRefToComment("file_comment") if got, want := ref.Channel, ""; got != want { t.Errorf("Channel got %s, want %s", got, want) } if got, want := ref.Timestamp, ""; got != want { t.Errorf("Timestamp got %s, want %s", got, want) } if got, want := ref.File, ""; got != want { t.Errorf("File got %s, want %s", got, want) } if got, want := ref.Comment, "file_comment"; got != want { t.Errorf("Comment got %s, want %s", got, want) } } golang-github-nlopes-slack-0.1.0/messageID.go000066400000000000000000000010271311262777100210270ustar00rootroot00000000000000package slack import "sync" // IDGenerator provides an interface for generating integer ID values. type IDGenerator interface { Next() int } // NewSafeID returns a new instance of an IDGenerator which is safe for // concurrent use by multiple goroutines. func NewSafeID(startID int) IDGenerator { return &safeID{ nextID: startID, mutex: &sync.Mutex{}, } } type safeID struct { nextID int mutex *sync.Mutex } func (s *safeID) Next() int { s.mutex.Lock() defer s.mutex.Unlock() id := s.nextID s.nextID++ return id } golang-github-nlopes-slack-0.1.0/messages.go000066400000000000000000000102761311262777100210030ustar00rootroot00000000000000package slack // OutgoingMessage is used for the realtime API, and seems incomplete. type OutgoingMessage struct { ID int `json:"id"` Channel string `json:"channel,omitempty"` Text string `json:"text,omitempty"` Type string `json:"type,omitempty"` ThreadTimestamp string `json:"thread_ts,omitempty"` } // Message is an auxiliary type to allow us to have a message containing sub messages type Message struct { Msg SubMessage *Msg `json:"message,omitempty"` } // Msg contains information about a slack message type Msg struct { // Basic Message Type string `json:"type,omitempty"` Channel string `json:"channel,omitempty"` User string `json:"user,omitempty"` Text string `json:"text,omitempty"` Timestamp string `json:"ts,omitempty"` ThreadTimestamp string `json:"thread_ts,omitempty"` IsStarred bool `json:"is_starred,omitempty"` PinnedTo []string `json:"pinned_to, omitempty"` Attachments []Attachment `json:"attachments,omitempty"` Edited *Edited `json:"edited,omitempty"` // Message Subtypes SubType string `json:"subtype,omitempty"` // Hidden Subtypes Hidden bool `json:"hidden,omitempty"` // message_changed, message_deleted, unpinned_item DeletedTimestamp string `json:"deleted_ts,omitempty"` // message_deleted EventTimestamp string `json:"event_ts,omitempty"` // bot_message (https://api.slack.com/events/message/bot_message) BotID string `json:"bot_id,omitempty"` Username string `json:"username,omitempty"` Icons *Icon `json:"icons,omitempty"` // channel_join, group_join Inviter string `json:"inviter,omitempty"` // channel_topic, group_topic Topic string `json:"topic,omitempty"` // channel_purpose, group_purpose Purpose string `json:"purpose,omitempty"` // channel_name, group_name Name string `json:"name,omitempty"` OldName string `json:"old_name,omitempty"` // channel_archive, group_archive Members []string `json:"members,omitempty"` // channels.replies, groups.replies, im.replies, mpim.replies ReplyCount int `json:"reply_count,omitempty"` Replies []Reply `json:"replies,omitempty"` ParentUserId string `json:"parent_user_id,omitempty"` // file_share, file_comment, file_mention File *File `json:"file,omitempty"` // file_share Upload bool `json:"upload,omitempty"` // file_comment Comment *Comment `json:"comment,omitempty"` // pinned_item ItemType string `json:"item_type,omitempty"` // https://api.slack.com/rtm ReplyTo int `json:"reply_to,omitempty"` Team string `json:"team,omitempty"` // reactions Reactions []ItemReaction `json:"reactions,omitempty"` } // Icon is used for bot messages type Icon struct { IconURL string `json:"icon_url,omitempty"` IconEmoji string `json:"icon_emoji,omitempty"` } // Edited indicates that a message has been edited. type Edited struct { User string `json:"user,omitempty"` Timestamp string `json:"ts,omitempty"` } // Reply contains information about a reply for a thread type Reply struct { User string `json:"user,omitempty"` Timestamp string `json:"ts,omitempty"` } // Event contains the event type type Event struct { Type string `json:"type,omitempty"` } // Ping contains information about a Ping Event type Ping struct { ID int `json:"id"` Type string `json:"type"` } // Pong contains information about a Pong Event type Pong struct { Type string `json:"type"` ReplyTo int `json:"reply_to"` } // 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, channel string) *OutgoingMessage { id := rtm.idGen.Next() return &OutgoingMessage{ ID: id, Type: "message", Channel: channel, Text: text, } } // NewTypingMessage prepares an OutgoingMessage that the user can // use to send as a typing indicator. Use this function to properly set the // messageID. func (rtm *RTM) NewTypingMessage(channel string) *OutgoingMessage { id := rtm.idGen.Next() return &OutgoingMessage{ ID: id, Type: "typing", Channel: channel, } } golang-github-nlopes-slack-0.1.0/messages_test.go000066400000000000000000000661571311262777100220530ustar00rootroot00000000000000package slack import ( "encoding/json" "fmt" "testing" "github.com/stretchr/testify/assert" ) var simpleMessage = `{ "type": "message", "channel": "C2147483705", "user": "U2147483697", "text": "Hello world", "ts": "1355517523.000005" }` func unmarshalMessage(j string) (*Message, error) { message := &Message{} if err := json.Unmarshal([]byte(j), &message); err != nil { return nil, err } return message, nil } func TestSimpleMessage(t *testing.T) { message, err := unmarshalMessage(simpleMessage) assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) assert.Equal(t, "C2147483705", message.Channel) assert.Equal(t, "U2147483697", message.User) assert.Equal(t, "Hello world", message.Text) assert.Equal(t, "1355517523.000005", message.Timestamp) } var starredMessage = `{ "text": "is testing", "type": "message", "subtype": "me_message", "user": "U2147483697", "ts": "1433314126.000003", "is_starred": true }` func TestStarredMessage(t *testing.T) { message, err := unmarshalMessage(starredMessage) assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "is testing", message.Text) assert.Equal(t, "message", message.Type) assert.Equal(t, "me_message", message.SubType) assert.Equal(t, "U2147483697", message.User) assert.Equal(t, "1433314126.000003", message.Timestamp) assert.Equal(t, true, message.IsStarred) } var editedMessage = `{ "type": "message", "user": "U2147483697", "text": "hello edited", "edited": { "user": "U2147483697", "ts": "1433314416.000000" }, "ts": "1433314408.000004" }` func TestEditedMessage(t *testing.T) { message, err := unmarshalMessage(editedMessage) assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) assert.Equal(t, "U2147483697", message.User) assert.Equal(t, "hello edited", message.Text) assert.NotNil(t, message.Edited) assert.Equal(t, "U2147483697", message.Edited.User) assert.Equal(t, "1433314416.000000", message.Edited.Timestamp) assert.Equal(t, "1433314408.000004", message.Timestamp) } var uploadedFile = `{ "type": "message", "subtype": "file_share", "text": "<@U2147483697|tester> uploaded a file: and commented: test comment here", "file": { "id": "abc", "created": 1433314757, "timestamp": 1433314757, "name": "test.txt", "title": "test.txt", "mimetype": "text\/plain", "filetype": "text", "pretty_type": "Plain Text", "user": "U2147483697", "editable": true, "size": 5, "mode": "snippet", "is_external": false, "external_type": "", "is_public": true, "public_url_shared": false, "url": "https:\/\/slack-files.com\/files-pub\/abc-def-ghi\/test.txt", "url_download": "https:\/\/slack-files.com\/files-pub\/abc-def-ghi\/download\/test.txt", "url_private": "https:\/\/files.slack.com\/files-pri\/abc-def\/test.txt", "url_private_download": "https:\/\/files.slack.com\/files-pri\/abc-def\/download\/test.txt", "permalink": "https:\/\/test.slack.com\/files\/tester\/abc\/test.txt", "permalink_public": "https:\/\/slack-files.com\/abc-def-ghi", "edit_link": "https:\/\/test.slack.com\/files\/tester\/abc\/test.txt\/edit", "preview": "test\n", "preview_highlight": "
test<\/pre><\/div>\n
<\/pre><\/div>\n<\/div>",
        "lines": 2,
        "lines_more": 0,
        "channels": [
            "C2147483705"
        ],
        "groups": [],
        "ims": [],
        "comments_count": 1,
        "initial_comment": {
            "id": "Fc066YLGKH",
            "created": 1433314757,
            "timestamp": 1433314757,
            "user": "U2147483697",
            "comment": "test comment here"
        }
    },
    "user": "U2147483697",
    "upload": true,
    "ts": "1433314757.000006"
}`

func TestUploadedFile(t *testing.T) {
	message, err := unmarshalMessage(uploadedFile)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "file_share", message.SubType)
	assert.Equal(t, "<@U2147483697|tester> uploaded a file:  and commented: test comment here", message.Text)
	// TODO: Assert File
	assert.Equal(t, "U2147483697", message.User)
	assert.True(t, message.Upload)
	assert.Equal(t, "1433314757.000006", message.Timestamp)
}

var testPost = `{
    "type": "message",
    "subtype": "file_share",
    "text": "<@U2147483697|tester> shared a file: ",
    "file": {
        "id": "abc",
        "created": 1433315398,
        "timestamp": 1433315398,
        "name": "test_post",
        "title": "test post",
        "mimetype": "text\/plain",
        "filetype": "post",
        "pretty_type": "Post",
        "user": "U2147483697",
        "editable": true,
        "size": 14,
        "mode": "post",
        "is_external": false,
        "external_type": "",
        "is_public": true,
        "public_url_shared": false,
        "url": "https:\/\/slack-files.com\/files-pub\/abc-def-ghi\/test_post",
        "url_download": "https:\/\/slack-files.com\/files-pub\/abc-def-ghi\/download\/test_post",
        "url_private": "https:\/\/files.slack.com\/files-pri\/abc-def\/test_post",
        "url_private_download": "https:\/\/files.slack.com\/files-pri\/abc-def\/download\/test_post",
        "permalink": "https:\/\/test.slack.com\/files\/tester\/abc\/test_post",
        "permalink_public": "https:\/\/slack-files.com\/abc-def-ghi",
        "edit_link": "https:\/\/test.slack.com\/files\/tester\/abc\/test_post\/edit",
        "preview": "test post body",
        "channels": [
            "C2147483705"
        ],
        "groups": [],
        "ims": [],
        "comments_count": 1
    },
    "user": "U2147483697",
    "upload": false,
    "ts": "1433315416.000008"
}`

func TestPost(t *testing.T) {
	message, err := unmarshalMessage(testPost)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "file_share", message.SubType)
	assert.Equal(t, "<@U2147483697|tester> shared a file: ", message.Text)
	// TODO: Assert File
	assert.Equal(t, "U2147483697", message.User)
	assert.False(t, message.Upload)
	assert.Equal(t, "1433315416.000008", message.Timestamp)
}

var testComment = `{
    "type": "message",
    "subtype": "file_comment",
    "text": "<@U2147483697|tester> commented on <@U2147483697|tester>'s file : another comment",
    "file": {
        "id": "abc",
        "created": 1433315398,
        "timestamp": 1433315398,
        "name": "test_post",
        "title": "test post",
        "mimetype": "text\/plain",
        "filetype": "post",
        "pretty_type": "Post",
        "user": "U2147483697",
        "editable": true,
        "size": 14,
        "mode": "post",
        "is_external": false,
        "external_type": "",
        "is_public": true,
        "public_url_shared": false,
        "url": "https:\/\/slack-files.com\/files-pub\/abc-def-ghi\/test_post",
        "url_download": "https:\/\/slack-files.com\/files-pub\/abc-def-ghi\/download\/test_post",
        "url_private": "https:\/\/files.slack.com\/files-pri\/abc-def\/test_post",
        "url_private_download": "https:\/\/files.slack.com\/files-pri\/abc-def\/download\/test_post",
        "permalink": "https:\/\/test.slack.com\/files\/tester\/abc\/test_post",
        "permalink_public": "https:\/\/slack-files.com\/abc-def-ghi",
        "edit_link": "https:\/\/test.slack.com\/files\/tester\/abc\/test_post\/edit",
        "preview": "test post body",
        "channels": [
            "C2147483705"
        ],
        "groups": [],
        "ims": [],
        "comments_count": 2
    },
    "comment": {
        "id": "xyz",
        "created": 1433316360,
        "timestamp": 1433316360,
        "user": "U2147483697",
        "comment": "another comment"
    },
    "ts": "1433316360.000009"
}`

func TestComment(t *testing.T) {
	message, err := unmarshalMessage(testComment)
	fmt.Println(err)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "file_comment", message.SubType)
	assert.Equal(t, "<@U2147483697|tester> commented on <@U2147483697|tester>'s file : another comment", message.Text)
	// TODO: Assert File
	// TODO: Assert Comment
	assert.Equal(t, "1433316360.000009", message.Timestamp)
}

var botMessage = `{
    "type": "message",
    "subtype": "bot_message",
    "ts": "1358877455.000010",
    "text": "Pushing is the answer",
    "bot_id": "BB12033",
    "username": "github",
    "icons": {}
}`

func TestBotMessage(t *testing.T) {
	message, err := unmarshalMessage(botMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "bot_message", message.SubType)
	assert.Equal(t, "1358877455.000010", message.Timestamp)
	assert.Equal(t, "Pushing is the answer", message.Text)
	assert.Equal(t, "BB12033", message.BotID)
	assert.Equal(t, "github", message.Username)
	assert.NotNil(t, message.Icons)
	assert.Empty(t, message.Icons.IconURL)
	assert.Empty(t, message.Icons.IconEmoji)
}

var meMessage = `{
    "type": "message",
    "subtype": "me_message",
    "channel": "C2147483705",
    "user": "U2147483697",
    "text": "is doing that thing",
    "ts": "1355517523.000005"
}`

func TestMeMessage(t *testing.T) {
	message, err := unmarshalMessage(meMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "me_message", message.SubType)
	assert.Equal(t, "C2147483705", message.Channel)
	assert.Equal(t, "U2147483697", message.User)
	assert.Equal(t, "is doing that thing", message.Text)
	assert.Equal(t, "1355517523.000005", message.Timestamp)
}

var messageChangedMessage = `{
    "type": "message",
    "subtype": "message_changed",
    "hidden": true,
    "channel": "C2147483705",
    "ts": "1358878755.000001",
    "message": {
        "type": "message",
        "user": "U2147483697",
        "text": "Hello, world!",
        "ts": "1355517523.000005",
        "edited": {
            "user": "U2147483697",
            "ts": "1358878755.000001"
        }
    }
}`

func TestMessageChangedMessage(t *testing.T) {
	message, err := unmarshalMessage(messageChangedMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "message_changed", message.SubType)
	assert.True(t, message.Hidden)
	assert.Equal(t, "C2147483705", message.Channel)
	assert.NotNil(t, message.SubMessage)
	assert.Equal(t, "message", message.SubMessage.Type)
	assert.Equal(t, "U2147483697", message.SubMessage.User)
	assert.Equal(t, "Hello, world!", message.SubMessage.Text)
	assert.Equal(t, "1355517523.000005", message.SubMessage.Timestamp)
	assert.NotNil(t, message.SubMessage.Edited)
	assert.Equal(t, "U2147483697", message.SubMessage.Edited.User)
	assert.Equal(t, "1358878755.000001", message.SubMessage.Edited.Timestamp)
	assert.Equal(t, "1358878755.000001", message.Timestamp)
}

var messageDeletedMessage = `{
    "type": "message",
    "subtype": "message_deleted",
    "hidden": true,
    "channel": "C2147483705",
    "ts": "1358878755.000001",
    "deleted_ts": "1358878749.000002"
}`

func TestMessageDeletedMessage(t *testing.T) {
	message, err := unmarshalMessage(messageDeletedMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "message_deleted", message.SubType)
	assert.True(t, message.Hidden)
	assert.Equal(t, "C2147483705", message.Channel)
	assert.Equal(t, "1358878755.000001", message.Timestamp)
	assert.Equal(t, "1358878749.000002", message.DeletedTimestamp)
}

var channelJoinMessage = `{
    "type": "message",
    "subtype": "channel_join",
    "ts": "1358877458.000011",
    "user": "U2147483828",
    "text": "<@U2147483828|cal> has joined the channel"
}`

func TestChannelJoinMessage(t *testing.T) {
	message, err := unmarshalMessage(channelJoinMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "channel_join", message.SubType)
	assert.Equal(t, "1358877458.000011", message.Timestamp)
	assert.Equal(t, "U2147483828", message.User)
	assert.Equal(t, "<@U2147483828|cal> has joined the channel", message.Text)
}

var channelJoinInvitedMessage = `{
    "type": "message",
    "subtype": "channel_join",
    "ts": "1358877458.000011",
    "user": "U2147483828",
    "text": "<@U2147483828|cal> has joined the channel",
		"inviter": "U2147483829"
}`

func TestChannelJoinInvitedMessage(t *testing.T) {
	message, err := unmarshalMessage(channelJoinInvitedMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "channel_join", message.SubType)
	assert.Equal(t, "1358877458.000011", message.Timestamp)
	assert.Equal(t, "U2147483828", message.User)
	assert.Equal(t, "<@U2147483828|cal> has joined the channel", message.Text)
	assert.Equal(t, "U2147483829", message.Inviter)
}

var channelLeaveMessage = `{
    "type": "message",
    "subtype": "channel_leave",
    "ts": "1358877455.000010",
    "user": "U2147483828",
    "text": "<@U2147483828|cal> has left the channel"
}`

func TestChannelLeaveMessage(t *testing.T) {
	message, err := unmarshalMessage(channelLeaveMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "channel_leave", message.SubType)
	assert.Equal(t, "1358877455.000010", message.Timestamp)
	assert.Equal(t, "U2147483828", message.User)
	assert.Equal(t, "<@U2147483828|cal> has left the channel", message.Text)
}

var channelTopicMessage = `{
    "type": "message",
    "subtype": "channel_topic",
    "ts": "1358877455.000010",
    "user": "U2147483828",
    "topic": "hello world",
    "text": "<@U2147483828|cal> set the channel topic: hello world"
}`

func TestChannelTopicMessage(t *testing.T) {
	message, err := unmarshalMessage(channelTopicMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "channel_topic", message.SubType)
	assert.Equal(t, "1358877455.000010", message.Timestamp)
	assert.Equal(t, "U2147483828", message.User)
	assert.Equal(t, "hello world", message.Topic)
	assert.Equal(t, "<@U2147483828|cal> set the channel topic: hello world", message.Text)
}

var channelPurposeMessage = `{
    "type": "message",
    "subtype": "channel_purpose",
    "ts": "1358877455.000010",
    "user": "U2147483828",
    "purpose": "whatever",
    "text": "<@U2147483828|cal> set the channel purpose: whatever"
}`

func TestChannelPurposeMessage(t *testing.T) {
	message, err := unmarshalMessage(channelPurposeMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "channel_purpose", message.SubType)
	assert.Equal(t, "1358877455.000010", message.Timestamp)
	assert.Equal(t, "U2147483828", message.User)
	assert.Equal(t, "whatever", message.Purpose)
	assert.Equal(t, "<@U2147483828|cal> set the channel purpose: whatever", message.Text)
}

var channelNameMessage = `{
    "type": "message",
    "subtype": "channel_name",
    "ts": "1358877455.000010",
    "user": "U2147483828",
    "old_name": "random",
    "name": "watercooler",
    "text": "<@U2147483828|cal> has renamed the channel from \"random\" to \"watercooler\""
}`

func TestChannelNameMessage(t *testing.T) {
	message, err := unmarshalMessage(channelNameMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "channel_name", message.SubType)
	assert.Equal(t, "1358877455.000010", message.Timestamp)
	assert.Equal(t, "U2147483828", message.User)
	assert.Equal(t, "random", message.OldName)
	assert.Equal(t, "watercooler", message.Name)
	assert.Equal(t, "<@U2147483828|cal> has renamed the channel from \"random\" to \"watercooler\"", message.Text)
}

var channelArchiveMessage = `{
    "type": "message",
    "subtype": "channel_archive",
    "ts": "1361482916.000003",
    "text": " archived the channel",
    "user": "U1234",
    "members": ["U1234", "U5678"]
}`

func TestChannelArchiveMessage(t *testing.T) {
	message, err := unmarshalMessage(channelArchiveMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "channel_archive", message.SubType)
	assert.Equal(t, "1361482916.000003", message.Timestamp)
	assert.Equal(t, " archived the channel", message.Text)
	assert.Equal(t, "U1234", message.User)
	assert.NotNil(t, message.Members)
	assert.Equal(t, 2, len(message.Members))
}

var channelUnarchiveMessage = `{
    "type": "message",
    "subtype": "channel_unarchive",
    "ts": "1361482916.000003",
    "text": " un-archived the channel",
    "user": "U1234"
}`

func TestChannelUnarchiveMessage(t *testing.T) {
	message, err := unmarshalMessage(channelUnarchiveMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "channel_unarchive", message.SubType)
	assert.Equal(t, "1361482916.000003", message.Timestamp)
	assert.Equal(t, " un-archived the channel", message.Text)
	assert.Equal(t, "U1234", message.User)
}

var channelRepliesParentMessage = `{
    "type": "message",
    "user": "U1234",
    "text": "test",
    "thread_ts": "1493305433.915644",
    "reply_count": 2,
    "replies": [
        {
            "user": "U5678",
            "ts": "1493305444.920992"
        },
        {
            "user": "U9012",
            "ts": "1493305894.133936"
        }
    ],
    "subscribed": true,
    "last_read": "1493305894.133936",
    "unread_count": 0,
    "ts": "1493305433.915644"
}`

func TestChannelRepliesParentMessage(t *testing.T) {
	message, err := unmarshalMessage(channelRepliesParentMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "U1234", message.User)
	assert.Equal(t, "test", message.Text)
	assert.Equal(t, "1493305433.915644", message.ThreadTimestamp)
	assert.Equal(t, 2, message.ReplyCount)
	assert.Equal(t, "U5678", message.Replies[0].User)
	assert.Equal(t, "1493305444.920992", message.Replies[0].Timestamp)
	assert.Equal(t, "U9012", message.Replies[1].User)
	assert.Equal(t, "1493305894.133936", message.Replies[1].Timestamp)
	assert.Equal(t, "1493305433.915644", message.Timestamp)
}

var channelRepliesChildMessage = `{
    "type": "message",
    "user": "U5678",
    "text": "foo",
    "thread_ts": "1493305433.915644",
    "parent_user_id": "U1234",
    "ts": "1493305444.920992"
}`

func TestChannelRepliesChildMessage(t *testing.T) {
	message, err := unmarshalMessage(channelRepliesChildMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "U5678", message.User)
	assert.Equal(t, "foo", message.Text)
	assert.Equal(t, "1493305433.915644", message.ThreadTimestamp)
	assert.Equal(t, "U1234", message.ParentUserId)
	assert.Equal(t, "1493305444.920992", message.Timestamp)
}

var groupJoinMessage = `{
    "type": "message",
    "subtype": "group_join",
    "ts": "1358877458.000011",
    "user": "U2147483828",
    "text": "<@U2147483828|cal> has joined the group"
}`

func TestGroupJoinMessage(t *testing.T) {
	message, err := unmarshalMessage(groupJoinMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "group_join", message.SubType)
	assert.Equal(t, "1358877458.000011", message.Timestamp)
	assert.Equal(t, "U2147483828", message.User)
	assert.Equal(t, "<@U2147483828|cal> has joined the group", message.Text)
}

var groupJoinInvitedMessage = `{
    "type": "message",
    "subtype": "group_join",
    "ts": "1358877458.000011",
    "user": "U2147483828",
    "text": "<@U2147483828|cal> has joined the group",
		"inviter": "U2147483829"
}`

func TestGroupJoinInvitedMessage(t *testing.T) {
	message, err := unmarshalMessage(groupJoinInvitedMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "group_join", message.SubType)
	assert.Equal(t, "1358877458.000011", message.Timestamp)
	assert.Equal(t, "U2147483828", message.User)
	assert.Equal(t, "<@U2147483828|cal> has joined the group", message.Text)
	assert.Equal(t, "U2147483829", message.Inviter)
}

var groupLeaveMessage = `{
    "type": "message",
    "subtype": "group_leave",
    "ts": "1358877455.000010",
    "user": "U2147483828",
    "text": "<@U2147483828|cal> has left the group"
}`

func TestGroupLeaveMessage(t *testing.T) {
	message, err := unmarshalMessage(groupLeaveMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "group_leave", message.SubType)
	assert.Equal(t, "1358877455.000010", message.Timestamp)
	assert.Equal(t, "U2147483828", message.User)
	assert.Equal(t, "<@U2147483828|cal> has left the group", message.Text)
}

var groupTopicMessage = `{
    "type": "message",
    "subtype": "group_topic",
    "ts": "1358877455.000010",
    "user": "U2147483828",
    "topic": "hello world",
    "text": "<@U2147483828|cal> set the group topic: hello world"
}`

func TestGroupTopicMessage(t *testing.T) {
	message, err := unmarshalMessage(groupTopicMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "group_topic", message.SubType)
	assert.Equal(t, "1358877455.000010", message.Timestamp)
	assert.Equal(t, "U2147483828", message.User)
	assert.Equal(t, "hello world", message.Topic)
	assert.Equal(t, "<@U2147483828|cal> set the group topic: hello world", message.Text)
}

var groupPurposeMessage = `{
    "type": "message",
    "subtype": "group_purpose",
    "ts": "1358877455.000010",
    "user": "U2147483828",
    "purpose": "whatever",
    "text": "<@U2147483828|cal> set the group purpose: whatever"
}`

func TestGroupPurposeMessage(t *testing.T) {
	message, err := unmarshalMessage(groupPurposeMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "group_purpose", message.SubType)
	assert.Equal(t, "1358877455.000010", message.Timestamp)
	assert.Equal(t, "U2147483828", message.User)
	assert.Equal(t, "whatever", message.Purpose)
	assert.Equal(t, "<@U2147483828|cal> set the group purpose: whatever", message.Text)
}

var groupNameMessage = `{
    "type": "message",
    "subtype": "group_name",
    "ts": "1358877455.000010",
    "user": "U2147483828",
    "old_name": "random",
    "name": "watercooler",
    "text": "<@U2147483828|cal> has renamed the group from \"random\" to \"watercooler\""
}`

func TestGroupNameMessage(t *testing.T) {
	message, err := unmarshalMessage(groupNameMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "group_name", message.SubType)
	assert.Equal(t, "1358877455.000010", message.Timestamp)
	assert.Equal(t, "U2147483828", message.User)
	assert.Equal(t, "random", message.OldName)
	assert.Equal(t, "watercooler", message.Name)
	assert.Equal(t, "<@U2147483828|cal> has renamed the group from \"random\" to \"watercooler\"", message.Text)
}

var groupArchiveMessage = `{
    "type": "message",
    "subtype": "group_archive",
    "ts": "1361482916.000003",
    "text": " archived the group",
    "user": "U1234",
    "members": ["U1234", "U5678"]
}`

func TestGroupArchiveMessage(t *testing.T) {
	message, err := unmarshalMessage(groupArchiveMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "group_archive", message.SubType)
	assert.Equal(t, "1361482916.000003", message.Timestamp)
	assert.Equal(t, " archived the group", message.Text)
	assert.Equal(t, "U1234", message.User)
	assert.NotNil(t, message.Members)
	assert.Equal(t, 2, len(message.Members))
}

var groupUnarchiveMessage = `{
    "type": "message",
    "subtype": "group_unarchive",
    "ts": "1361482916.000003",
    "text": " un-archived the group",
    "user": "U1234"
}`

func TestGroupUnarchiveMessage(t *testing.T) {
	message, err := unmarshalMessage(groupUnarchiveMessage)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "group_unarchive", message.SubType)
	assert.Equal(t, "1361482916.000003", message.Timestamp)
	assert.Equal(t, " un-archived the group", message.Text)
	assert.Equal(t, "U1234", message.User)
}

var fileShareMessage = `{
    "type": "message",
    "subtype": "file_share",
    "ts": "1358877455.000010",
    "text": "<@cal> uploaded a file: ",
    "file": {
        "id" : "F2147483862",
        "created" : 1356032811,
        "timestamp" : 1356032811,
        "name" : "file.htm",
        "title" : "My HTML file",
        "mimetype" : "text\/plain",
        "filetype" : "text",
        "pretty_type": "Text",
        "user" : "U2147483697",
        "mode" : "hosted",
        "editable" : true,
        "is_external": false,
        "external_type": "",
        "size" : 12345,
        "url": "https:\/\/slack-files.com\/files-pub\/T024BE7LD-F024BERPE-09acb6\/1.png",
        "url_download": "https:\/\/slack-files.com\/files-pub\/T024BE7LD-F024BERPE-09acb6\/download\/1.png",
        "url_private": "https:\/\/slack.com\/files-pri\/T024BE7LD-F024BERPE\/1.png",
        "url_private_download": "https:\/\/slack.com\/files-pri\/T024BE7LD-F024BERPE\/download\/1.png",
        "thumb_64": "https:\/\/slack-files.com\/files-tmb\/T024BE7LD-F024BERPE-c66246\/1_64.png",
        "thumb_80": "https:\/\/slack-files.com\/files-tmb\/T024BE7LD-F024BERPE-c66246\/1_80.png",
        "thumb_360": "https:\/\/slack-files.com\/files-tmb\/T024BE7LD-F024BERPE-c66246\/1_360.png",
        "thumb_360_gif": "https:\/\/slack-files.com\/files-tmb\/T024BE7LD-F024BERPE-c66246\/1_360.gif",
        "thumb_360_w": 100,
        "thumb_360_h": 100,
        "permalink" : "https:\/\/tinyspeck.slack.com\/files\/cal\/F024BERPE\/1.png",
        "edit_link" : "https:\/\/tinyspeck.slack.com\/files\/cal\/F024BERPE\/1.png/edit",
        "preview" : "<!DOCTYPE html>\n<html>\n<meta charset='utf-8'>",
        "preview_highlight" : "<div class=\"sssh-code\"><div class=\"sssh-line\"><pre><!DOCTYPE html...",
        "lines" : 123,
        "lines_more": 118,
        "is_public": true,
        "public_url_shared": false,
        "channels": ["C024BE7LT"],
        "groups": ["G12345"],
        "ims": ["D12345"],
        "initial_comment": {},
        "num_stars": 7,
        "is_starred": true
    },
    "user": "U2147483697",
    "upload": true
}`

func TestFileShareMessage(t *testing.T) {
	message, err := unmarshalMessage(fileShareMessage)
	fmt.Println(err)
	assert.Nil(t, err)
	assert.NotNil(t, message)
	assert.Equal(t, "message", message.Type)
	assert.Equal(t, "file_share", message.SubType)
	assert.Equal(t, "1358877455.000010", message.Timestamp)
	assert.Equal(t, "<@cal> uploaded a file: ", message.Text)
	assert.Equal(t, "U2147483697", message.User)
	assert.True(t, message.Upload)
	assert.NotNil(t, message.File)
}
golang-github-nlopes-slack-0.1.0/misc.go000066400000000000000000000105141311262777100201220ustar00rootroot00000000000000package slack

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"net/http/httputil"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"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)
}

var customHTTPClient HTTPRequester

// HTTPClient sets a custom http.Client
// deprecated: in favor of SetHTTPClient()
var HTTPClient = &http.Client{}

type WebResponse struct {
	Ok    bool      `json:"ok"`
	Error *WebError `json:"error"`
}

type WebError string

func (s WebError) Error() string {
	return string(s)
}

func fileUploadReq(path, fieldname, filename string, values url.Values, r io.Reader) (*http.Request, error) {
	body := &bytes.Buffer{}
	wr := multipart.NewWriter(body)

	ioWriter, err := wr.CreateFormFile(fieldname, filename)
	if err != nil {
		wr.Close()
		return nil, err
	}
	_, err = io.Copy(ioWriter, r)
	if err != nil {
		wr.Close()
		return nil, err
	}
	// Close the multipart writer or the footer won't be written
	wr.Close()
	req, err := http.NewRequest("POST", path, body)
	if err != nil {
		return nil, err
	}
	req.Header.Add("Content-Type", wr.FormDataContentType())
	req.URL.RawQuery = (values).Encode()
	return req, nil
}

func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error {
	response, err := ioutil.ReadAll(body)
	if err != nil {
		return err
	}

	// FIXME: will be api.Debugf
	if debug {
		logger.Printf("parseResponseBody: %s\n", string(response))
	}

	err = json.Unmarshal(response, &intf)
	if err != nil {
		return err
	}

	return nil
}

func postLocalWithMultipartResponse(path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error {
	fullpath, err := filepath.Abs(fpath)
	if err != nil {
		return err
	}
	file, err := os.Open(fullpath)
	if err != nil {
		return err
	}
	defer file.Close()
	return postWithMultipartResponse(path, filepath.Base(fpath), fieldname, values, file, intf, debug)
}

func postWithMultipartResponse(path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, debug bool) error {
	req, err := fileUploadReq(SLACK_API+path, fieldname, name, values, r)
	if err != nil {
		return err
	}
	resp, err := getHTTPClient().Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
	if resp.StatusCode != 200 {
		logResponse(resp, debug)
		return fmt.Errorf("Slack server error: %s.", resp.Status)
	}

	return parseResponseBody(resp.Body, &intf, debug)
}

func postForm(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")

	resp, err := getHTTPClient().Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
	if resp.StatusCode != 200 {
		logResponse(resp, debug)
		return fmt.Errorf("Slack server error: %s.", resp.Status)
	}

	return parseResponseBody(resp.Body, &intf, debug)
}

func post(path string, values url.Values, intf interface{}, debug bool) error {
	return postForm(SLACK_API+path, values, intf, debug)
}

func parseAdminResponse(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(endpoint, values, intf, debug)
}

func logResponse(resp *http.Response, debug bool) error {
	if debug {
		text, err := httputil.DumpResponse(resp, true)
		if err != nil {
			return err
		}

		logger.Print(string(text))
	}

	return nil
}

func getHTTPClient() HTTPRequester {
	if customHTTPClient != nil {
		return customHTTPClient
	}

	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
}
golang-github-nlopes-slack-0.1.0/misc_test.go000066400000000000000000000040771311262777100211700ustar00rootroot00000000000000package slack

import (
	"log"
	"net/http"
	"net/url"
	"sync"
	"testing"
)

var (
	parseResponseOnce sync.Once
)

func parseResponseHandler(rw http.ResponseWriter, r *http.Request) {
	rw.Header().Set("Content-Type", "application/json")
	token := r.FormValue("token")
	log.Println(token)
	if token == "" {
		rw.Write([]byte(`{"ok":false,"error":"not_authed"}`))
		return
	}
	if token != validToken {
		rw.Write([]byte(`{"ok":false,"error":"invalid_auth"}`))
		return
	}
	response := []byte(`{"ok": true}`)
	rw.Write(response)
}

func setParseResponseHandler() {
	http.HandleFunc("/parseResponse", parseResponseHandler)
}

func TestParseResponse(t *testing.T) {
	parseResponseOnce.Do(setParseResponseHandler)
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	values := url.Values{
		"token": {validToken},
	}
	responsePartial := &SlackResponse{}
	err := post("parseResponse", values, responsePartial, false)
	if err != nil {
		t.Errorf("Unexpected error: %s", err)
	}
}

func TestParseResponseNoToken(t *testing.T) {
	parseResponseOnce.Do(setParseResponseHandler)
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	values := url.Values{}
	responsePartial := &SlackResponse{}
	err := post("parseResponse", values, responsePartial, false)
	if err != nil {
		t.Errorf("Unexpected error: %s", err)
		return
	}
	if responsePartial.Ok == true {
		t.Errorf("Unexpected error: %s", err)
	} else if responsePartial.Error != "not_authed" {
		t.Errorf("got %v; want %v", responsePartial.Error, "not_authed")
	}
}

func TestParseResponseInvalidToken(t *testing.T) {
	parseResponseOnce.Do(setParseResponseHandler)
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	values := url.Values{
		"token": {"whatever"},
	}
	responsePartial := &SlackResponse{}
	err := post("parseResponse", values, responsePartial, false)
	if err != nil {
		t.Errorf("Unexpected error: %s", err)
		return
	}
	if responsePartial.Ok == true {
		t.Errorf("Unexpected error: %s", err)
	} else if responsePartial.Error != "invalid_auth" {
		t.Errorf("got %v; want %v", responsePartial.Error, "invalid_auth")
	}
}
golang-github-nlopes-slack-0.1.0/oauth.go000066400000000000000000000033211311262777100203050ustar00rootroot00000000000000package slack

import (
	"errors"
	"net/url"
)

type OAuthResponseIncomingWebhook struct {
	URL              string `json:"url"`
	Channel          string `json:"channel"`
	ChannelID        string `json:"channel_id,omitempty"`
	ConfigurationURL string `json:"configuration_url"`
}

type OAuthResponseBot struct {
	BotUserID      string `json:"bot_user_id"`
	BotAccessToken string `json:"bot_access_token"`
}

type OAuthResponse struct {
	AccessToken     string                       `json:"access_token"`
	Scope           string                       `json:"scope"`
	TeamName        string                       `json:"team_name"`
	TeamID          string                       `json:"team_id"`
	IncomingWebhook OAuthResponseIncomingWebhook `json:"incoming_webhook"`
	Bot             OAuthResponseBot             `json:"bot"`
	UserID          string                       `json:"user_id,omitempty"`
	SlackResponse
}

// GetOAuthToken retrieves an AccessToken
func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) {
	response, err := GetOAuthResponse(clientID, clientSecret, code, redirectURI, debug)
	if err != nil {
		return "", "", err
	}
	return response.AccessToken, response.Scope, nil
}

func GetOAuthResponse(clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) {
	values := url.Values{
		"client_id":     {clientID},
		"client_secret": {clientSecret},
		"code":          {code},
		"redirect_uri":  {redirectURI},
	}
	response := &OAuthResponse{}
	err = post("oauth.access", values, response, debug)
	if err != nil {
		return nil, err
	}
	if !response.Ok {
		return nil, errors.New(response.Error)
	}
	return response, nil
}
golang-github-nlopes-slack-0.1.0/pagination.go000066400000000000000000000010131311262777100213120ustar00rootroot00000000000000package slack

// Paging contains paging information
type Paging struct {
	Count int `json:"count"`
	Total int `json:"total"`
	Page  int `json:"page"`
	Pages int `json:"pages"`
}

// Pagination contains pagination information
// This is different from Paging in that it contains additional details
type Pagination struct {
	TotalCount int `json:"total_count"`
	Page       int `json:"page"`
	PerPage    int `json:"per_page"`
	PageCount  int `json:"page_count"`
	First      int `json:"first"`
	Last       int `json:"last"`
}
golang-github-nlopes-slack-0.1.0/pins.go000066400000000000000000000034341311262777100201430ustar00rootroot00000000000000package slack

import (
	"errors"
	"net/url"
)

type listPinsResponseFull struct {
	Items  []Item
	Paging `json:"paging"`
	SlackResponse
}

// AddPin pins an item in a channel
func (api *Client) AddPin(channel string, item ItemRef) error {
	values := url.Values{
		"channel": {channel},
		"token":   {api.config.token},
	}
	if item.Timestamp != "" {
		values.Set("timestamp", string(item.Timestamp))
	}
	if item.File != "" {
		values.Set("file", string(item.File))
	}
	if item.Comment != "" {
		values.Set("file_comment", string(item.Comment))
	}
	response := &SlackResponse{}
	if err := post("pins.add", values, response, api.debug); err != nil {
		return err
	}
	if !response.Ok {
		return errors.New(response.Error)
	}
	return nil
}

// RemovePin un-pins an item from a channel
func (api *Client) RemovePin(channel string, item ItemRef) error {
	values := url.Values{
		"channel": {channel},
		"token":   {api.config.token},
	}
	if item.Timestamp != "" {
		values.Set("timestamp", string(item.Timestamp))
	}
	if item.File != "" {
		values.Set("file", string(item.File))
	}
	if item.Comment != "" {
		values.Set("file_comment", string(item.Comment))
	}
	response := &SlackResponse{}
	if err := post("pins.remove", values, response, api.debug); err != nil {
		return err
	}
	if !response.Ok {
		return errors.New(response.Error)
	}
	return nil
}

// ListPins returns information about the items a user reacted to.
func (api *Client) ListPins(channel string) ([]Item, *Paging, error) {
	values := url.Values{
		"channel": {channel},
		"token":   {api.config.token},
	}
	response := &listPinsResponseFull{}
	err := post("pins.list", values, response, api.debug)
	if err != nil {
		return nil, nil, err
	}
	if !response.Ok {
		return nil, nil, errors.New(response.Error)
	}
	return response.Items, &response.Paging, nil
}
golang-github-nlopes-slack-0.1.0/pins_test.go000066400000000000000000000132711311262777100212020ustar00rootroot00000000000000package slack

import (
	"fmt"
	"net/http"
	"reflect"
	"testing"
)

type pinsHandler struct {
	gotParams map[string]string
	response  string
}

func newPinsHandler() *pinsHandler {
	return &pinsHandler{
		gotParams: make(map[string]string),
		response:  `{ "ok": true }`,
	}
}

func (rh *pinsHandler) accumulateFormValue(k string, r *http.Request) {
	if v := r.FormValue(k); v != "" {
		rh.gotParams[k] = v
	}
}

func (rh *pinsHandler) handler(w http.ResponseWriter, r *http.Request) {
	rh.accumulateFormValue("channel", r)
	rh.accumulateFormValue("count", r)
	rh.accumulateFormValue("file", r)
	rh.accumulateFormValue("file_comment", r)
	rh.accumulateFormValue("page", r)
	rh.accumulateFormValue("timestamp", r)
	w.Header().Set("Content-Type", "application/json")
	w.Write([]byte(rh.response))
}

func TestSlack_AddPin(t *testing.T) {
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")
	tests := []struct {
		channel    string
		ref        ItemRef
		wantParams map[string]string
	}{
		{
			"ChannelID",
			NewRefToMessage("ChannelID", "123"),
			map[string]string{
				"channel":   "ChannelID",
				"timestamp": "123",
			},
		},
		{
			"ChannelID",
			NewRefToFile("FileID"),
			map[string]string{
				"channel": "ChannelID",
				"file":    "FileID",
			},
		},
		{
			"ChannelID",
			NewRefToComment("FileCommentID"),
			map[string]string{
				"channel":      "ChannelID",
				"file_comment": "FileCommentID",
			},
		},
	}
	var rh *pinsHandler
	http.HandleFunc("/pins.add", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
	for i, test := range tests {
		rh = newPinsHandler()
		err := api.AddPin(test.channel, test.ref)
		if err != nil {
			t.Fatalf("%d: Unexpected error: %s", i, err)
		}
		if !reflect.DeepEqual(rh.gotParams, test.wantParams) {
			t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams)
		}
	}
}

func TestSlack_RemovePin(t *testing.T) {
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")
	tests := []struct {
		channel    string
		ref        ItemRef
		wantParams map[string]string
	}{
		{
			"ChannelID",
			NewRefToMessage("ChannelID", "123"),
			map[string]string{
				"channel":   "ChannelID",
				"timestamp": "123",
			},
		},
		{
			"ChannelID",
			NewRefToFile("FileID"),
			map[string]string{
				"channel": "ChannelID",
				"file":    "FileID",
			},
		},
		{
			"ChannelID",
			NewRefToComment("FileCommentID"),
			map[string]string{
				"channel":      "ChannelID",
				"file_comment": "FileCommentID",
			},
		},
	}
	var rh *pinsHandler
	http.HandleFunc("/pins.remove", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
	for i, test := range tests {
		rh = newPinsHandler()
		err := api.RemovePin(test.channel, test.ref)
		if err != nil {
			t.Fatalf("%d: Unexpected error: %s", i, err)
		}
		if !reflect.DeepEqual(rh.gotParams, test.wantParams) {
			t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams)
		}
	}
}

func TestSlack_ListPins(t *testing.T) {
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")
	rh := newPinsHandler()
	http.HandleFunc("/pins.list", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
	rh.response = `{"ok": true,
    "items": [
        {
            "type": "message",
            "channel": "C1",
            "message": {
                "text": "hello",
                "reactions": [
                    {
                        "name": "astonished",
                        "count": 3,
                        "users": [ "U1", "U2", "U3" ]
                    },
                    {
                        "name": "clock1",
                        "count": 3,
                        "users": [ "U1", "U2" ]
                    }
                ]
            }
        },
        {
            "type": "file",
            "file": {
                "name": "toy",
                "reactions": [
                    {
                        "name": "clock1",
                        "count": 3,
                        "users": [ "U1", "U2" ]
                    }
                ]
            }
        },
        {
            "type": "file_comment",
            "file": {
                "name": "toy"
            },
            "comment": {
                "comment": "cool toy",
                "reactions": [
                    {
                        "name": "astonished",
                        "count": 3,
                        "users": [ "U1", "U2", "U3" ]
                    }
                ]
            }
        }
    ],
    "paging": {
        "count": 100,
        "total": 4,
        "page": 1,
        "pages": 1
    }}`
	want := []Item{
		NewMessageItem("C1", &Message{Msg: Msg{
			Text: "hello",
			Reactions: []ItemReaction{
				ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
				ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
			},
		}}),
		NewFileItem(&File{Name: "toy"}),
		NewFileCommentItem(&File{Name: "toy"}, &Comment{Comment: "cool toy"}),
	}
	wantParams := map[string]string{
		"channel": "ChannelID",
	}
	got, paging, err := api.ListPins("ChannelID")
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("Got Pins %#v, want %#v", got, want)
		for i, item := range got {
			fmt.Printf("Item %d, Type: %s\n", i, item.Type)
			fmt.Printf("Message  %#v\n", item.Message)
			fmt.Printf("File     %#v\n", item.File)
			fmt.Printf("Comment  %#v\n", item.Comment)
		}
	}
	if !reflect.DeepEqual(rh.gotParams, wantParams) {
		t.Errorf("Got params %#v, want %#v", rh.gotParams, wantParams)
	}
	if reflect.DeepEqual(paging, Paging{}) {
		t.Errorf("Want paging data, got empty struct")
	}
}
golang-github-nlopes-slack-0.1.0/reactions.go000066400000000000000000000135511311262777100211620ustar00rootroot00000000000000package slack

import (
	"errors"
	"net/url"
	"strconv"
)

// ItemReaction is the reactions that have happened on an item.
type ItemReaction struct {
	Name  string   `json:"name"`
	Count int      `json:"count"`
	Users []string `json:"users"`
}

// ReactedItem is an item that was reacted to, and the details of the
// reactions.
type ReactedItem struct {
	Item
	Reactions []ItemReaction
}

// GetReactionsParameters is the inputs to get reactions to an item.
type GetReactionsParameters struct {
	Full bool
}

// NewGetReactionsParameters initializes the inputs to get reactions to an item.
func NewGetReactionsParameters() GetReactionsParameters {
	return GetReactionsParameters{
		Full: false,
	}
}

type getReactionsResponseFull struct {
	Type string
	M    struct {
		Reactions []ItemReaction
	} `json:"message"`
	F struct {
		Reactions []ItemReaction
	} `json:"file"`
	FC struct {
		Reactions []ItemReaction
	} `json:"comment"`
	SlackResponse
}

func (res getReactionsResponseFull) extractReactions() []ItemReaction {
	switch res.Type {
	case "message":
		return res.M.Reactions
	case "file":
		return res.F.Reactions
	case "file_comment":
		return res.FC.Reactions
	}
	return []ItemReaction{}
}

const (
	DEFAULT_REACTIONS_USER  = ""
	DEFAULT_REACTIONS_COUNT = 100
	DEFAULT_REACTIONS_PAGE  = 1
	DEFAULT_REACTIONS_FULL  = false
)

// ListReactionsParameters is the inputs to find all reactions by a user.
type ListReactionsParameters struct {
	User  string
	Count int
	Page  int
	Full  bool
}

// NewListReactionsParameters initializes the inputs to find all reactions
// performed by a user.
func NewListReactionsParameters() ListReactionsParameters {
	return ListReactionsParameters{
		User:  DEFAULT_REACTIONS_USER,
		Count: DEFAULT_REACTIONS_COUNT,
		Page:  DEFAULT_REACTIONS_PAGE,
		Full:  DEFAULT_REACTIONS_FULL,
	}
}

type listReactionsResponseFull struct {
	Items []struct {
		Type    string
		Channel string
		M       struct {
			*Message
		} `json:"message"`
		F struct {
			*File
			Reactions []ItemReaction
		} `json:"file"`
		FC struct {
			*Comment
			Reactions []ItemReaction
		} `json:"comment"`
	}
	Paging `json:"paging"`
	SlackResponse
}

func (res listReactionsResponseFull) extractReactedItems() []ReactedItem {
	items := make([]ReactedItem, len(res.Items))
	for i, input := range res.Items {
		item := ReactedItem{}
		item.Type = input.Type
		switch input.Type {
		case "message":
			item.Channel = input.Channel
			item.Message = input.M.Message
			item.Reactions = input.M.Reactions
		case "file":
			item.File = input.F.File
			item.Reactions = input.F.Reactions
		case "file_comment":
			item.File = input.F.File
			item.Comment = input.FC.Comment
			item.Reactions = input.FC.Reactions
		}
		items[i] = item
	}
	return items
}

// AddReaction adds a reaction emoji to a message, file or file comment.
func (api *Client) AddReaction(name string, item ItemRef) error {
	values := url.Values{
		"token": {api.config.token},
	}
	if name != "" {
		values.Set("name", name)
	}
	if item.Channel != "" {
		values.Set("channel", string(item.Channel))
	}
	if item.Timestamp != "" {
		values.Set("timestamp", string(item.Timestamp))
	}
	if item.File != "" {
		values.Set("file", string(item.File))
	}
	if item.Comment != "" {
		values.Set("file_comment", string(item.Comment))
	}
	response := &SlackResponse{}
	if err := post("reactions.add", values, response, api.debug); err != nil {
		return err
	}
	if !response.Ok {
		return errors.New(response.Error)
	}
	return nil
}

// RemoveReaction removes a reaction emoji from a message, file or file comment.
func (api *Client) RemoveReaction(name string, item ItemRef) error {
	values := url.Values{
		"token": {api.config.token},
	}
	if name != "" {
		values.Set("name", name)
	}
	if item.Channel != "" {
		values.Set("channel", string(item.Channel))
	}
	if item.Timestamp != "" {
		values.Set("timestamp", string(item.Timestamp))
	}
	if item.File != "" {
		values.Set("file", string(item.File))
	}
	if item.Comment != "" {
		values.Set("file_comment", string(item.Comment))
	}
	response := &SlackResponse{}
	if err := post("reactions.remove", values, response, api.debug); err != nil {
		return err
	}
	if !response.Ok {
		return errors.New(response.Error)
	}
	return nil
}

// GetReactions returns details about the reactions on an item.
func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) {
	values := url.Values{
		"token": {api.config.token},
	}
	if item.Channel != "" {
		values.Set("channel", string(item.Channel))
	}
	if item.Timestamp != "" {
		values.Set("timestamp", string(item.Timestamp))
	}
	if item.File != "" {
		values.Set("file", string(item.File))
	}
	if item.Comment != "" {
		values.Set("file_comment", string(item.Comment))
	}
	if params.Full != DEFAULT_REACTIONS_FULL {
		values.Set("full", strconv.FormatBool(params.Full))
	}
	response := &getReactionsResponseFull{}
	if err := post("reactions.get", values, response, api.debug); err != nil {
		return nil, err
	}
	if !response.Ok {
		return nil, errors.New(response.Error)
	}
	return response.extractReactions(), nil
}

// ListReactions returns information about the items a user reacted to.
func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, *Paging, error) {
	values := url.Values{
		"token": {api.config.token},
	}
	if params.User != DEFAULT_REACTIONS_USER {
		values.Add("user", params.User)
	}
	if params.Count != DEFAULT_REACTIONS_COUNT {
		values.Add("count", strconv.Itoa(params.Count))
	}
	if params.Page != DEFAULT_REACTIONS_PAGE {
		values.Add("page", strconv.Itoa(params.Page))
	}
	if params.Full != DEFAULT_REACTIONS_FULL {
		values.Add("full", strconv.FormatBool(params.Full))
	}
	response := &listReactionsResponseFull{}
	err := post("reactions.list", values, response, api.debug)
	if err != nil {
		return nil, nil, err
	}
	if !response.Ok {
		return nil, nil, errors.New(response.Error)
	}
	return response.extractReactedItems(), &response.Paging, nil
}
golang-github-nlopes-slack-0.1.0/reactions_test.go000066400000000000000000000231341311262777100222170ustar00rootroot00000000000000package slack

import (
	"fmt"
	"net/http"
	"reflect"
	"testing"
)

type reactionsHandler struct {
	gotParams map[string]string
	response  string
}

func newReactionsHandler() *reactionsHandler {
	return &reactionsHandler{
		gotParams: make(map[string]string),
		response:  `{ "ok": true }`,
	}
}

func (rh *reactionsHandler) accumulateFormValue(k string, r *http.Request) {
	if v := r.FormValue(k); v != "" {
		rh.gotParams[k] = v
	}
}

func (rh *reactionsHandler) handler(w http.ResponseWriter, r *http.Request) {
	rh.accumulateFormValue("channel", r)
	rh.accumulateFormValue("count", r)
	rh.accumulateFormValue("file", r)
	rh.accumulateFormValue("file_comment", r)
	rh.accumulateFormValue("full", r)
	rh.accumulateFormValue("name", r)
	rh.accumulateFormValue("page", r)
	rh.accumulateFormValue("timestamp", r)
	rh.accumulateFormValue("user", r)
	w.Header().Set("Content-Type", "application/json")
	w.Write([]byte(rh.response))
}

func TestSlack_AddReaction(t *testing.T) {
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")
	tests := []struct {
		name       string
		ref        ItemRef
		wantParams map[string]string
	}{
		{
			"thumbsup",
			NewRefToMessage("ChannelID", "123"),
			map[string]string{
				"name":      "thumbsup",
				"channel":   "ChannelID",
				"timestamp": "123",
			},
		},
		{
			"thumbsup",
			NewRefToFile("FileID"),
			map[string]string{
				"name": "thumbsup",
				"file": "FileID",
			},
		},
		{
			"thumbsup",
			NewRefToComment("FileCommentID"),
			map[string]string{
				"name":         "thumbsup",
				"file_comment": "FileCommentID",
			},
		},
	}
	var rh *reactionsHandler
	http.HandleFunc("/reactions.add", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
	for i, test := range tests {
		rh = newReactionsHandler()
		err := api.AddReaction(test.name, test.ref)
		if err != nil {
			t.Fatalf("%d: Unexpected error: %s", i, err)
		}
		if !reflect.DeepEqual(rh.gotParams, test.wantParams) {
			t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams)
		}
	}
}

func TestSlack_RemoveReaction(t *testing.T) {
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")
	tests := []struct {
		name       string
		ref        ItemRef
		wantParams map[string]string
	}{
		{
			"thumbsup",
			NewRefToMessage("ChannelID", "123"),
			map[string]string{
				"name":      "thumbsup",
				"channel":   "ChannelID",
				"timestamp": "123",
			},
		},
		{
			"thumbsup",
			NewRefToFile("FileID"),
			map[string]string{
				"name": "thumbsup",
				"file": "FileID",
			},
		},
		{
			"thumbsup",
			NewRefToComment("FileCommentID"),
			map[string]string{
				"name":         "thumbsup",
				"file_comment": "FileCommentID",
			},
		},
	}
	var rh *reactionsHandler
	http.HandleFunc("/reactions.remove", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
	for i, test := range tests {
		rh = newReactionsHandler()
		err := api.RemoveReaction(test.name, test.ref)
		if err != nil {
			t.Fatalf("%d: Unexpected error: %s", i, err)
		}
		if !reflect.DeepEqual(rh.gotParams, test.wantParams) {
			t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams)
		}
	}
}

func TestSlack_GetReactions(t *testing.T) {
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")
	tests := []struct {
		ref           ItemRef
		params        GetReactionsParameters
		wantParams    map[string]string
		json          string
		wantReactions []ItemReaction
	}{
		{
			NewRefToMessage("ChannelID", "123"),
			GetReactionsParameters{},
			map[string]string{
				"channel":   "ChannelID",
				"timestamp": "123",
			},
			`{"ok": true,
    "type": "message",
    "message": {
        "reactions": [
            {
                "name": "astonished",
                "count": 3,
                "users": [ "U1", "U2", "U3" ]
            },
            {
                "name": "clock1",
                "count": 3,
                "users": [ "U1", "U2" ]
            }
        ]
    }}`,
			[]ItemReaction{
				ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
				ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
			},
		},
		{
			NewRefToFile("FileID"),
			GetReactionsParameters{Full: true},
			map[string]string{
				"file": "FileID",
				"full": "true",
			},
			`{"ok": true,
    "type": "file",
    "file": {
        "reactions": [
            {
                "name": "astonished",
                "count": 3,
                "users": [ "U1", "U2", "U3" ]
            },
            {
                "name": "clock1",
                "count": 3,
                "users": [ "U1", "U2" ]
            }
        ]
    }}`,
			[]ItemReaction{
				ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
				ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
			},
		},
		{

			NewRefToComment("FileCommentID"),
			GetReactionsParameters{},
			map[string]string{
				"file_comment": "FileCommentID",
			},
			`{"ok": true,
    "type": "file_comment",
    "file": {},
    "comment": {
        "reactions": [
            {
                "name": "astonished",
                "count": 3,
                "users": [ "U1", "U2", "U3" ]
            },
            {
                "name": "clock1",
                "count": 3,
                "users": [ "U1", "U2" ]
            }
        ]
    }}`,
			[]ItemReaction{
				ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
				ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
			},
		},
	}
	var rh *reactionsHandler
	http.HandleFunc("/reactions.get", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
	for i, test := range tests {
		rh = newReactionsHandler()
		rh.response = test.json
		got, err := api.GetReactions(test.ref, test.params)
		if err != nil {
			t.Fatalf("%d: Unexpected error: %s", i, err)
		}
		if !reflect.DeepEqual(got, test.wantReactions) {
			t.Errorf("%d: Got reaction %#v, want %#v", i, got, test.wantReactions)
		}
		if !reflect.DeepEqual(rh.gotParams, test.wantParams) {
			t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams)
		}
	}
}

func TestSlack_ListReactions(t *testing.T) {
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")
	rh := newReactionsHandler()
	http.HandleFunc("/reactions.list", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
	rh.response = `{"ok": true,
    "items": [
        {
            "type": "message",
            "channel": "C1",
            "message": {
                "text": "hello",
                "reactions": [
                    {
                        "name": "astonished",
                        "count": 3,
                        "users": [ "U1", "U2", "U3" ]
                    },
                    {
                        "name": "clock1",
                        "count": 3,
                        "users": [ "U1", "U2" ]
                    }
                ]
            }
        },
        {
            "type": "file",
            "file": {
                "name": "toy",
                "reactions": [
                    {
                        "name": "clock1",
                        "count": 3,
                        "users": [ "U1", "U2" ]
                    }
                ]
            }
        },
        {
            "type": "file_comment",
            "file": {
                "name": "toy"
            },
            "comment": {
                "comment": "cool toy",
                "reactions": [
                    {
                        "name": "astonished",
                        "count": 3,
                        "users": [ "U1", "U2", "U3" ]
                    }
                ]
            }
        }
    ],
    "paging": {
        "count": 100,
        "total": 4,
        "page": 1,
        "pages": 1
    }}`
	want := []ReactedItem{
		ReactedItem{
			Item: NewMessageItem("C1", &Message{Msg: Msg{
				Text: "hello",
				Reactions: []ItemReaction{
					ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
					ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
				},
			}}),
			Reactions: []ItemReaction{
				ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
				ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
			},
		},
		ReactedItem{
			Item: NewFileItem(&File{Name: "toy"}),
			Reactions: []ItemReaction{
				ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
			},
		},
		ReactedItem{
			Item: NewFileCommentItem(&File{Name: "toy"}, &Comment{Comment: "cool toy"}),
			Reactions: []ItemReaction{
				ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
			},
		},
	}
	wantParams := map[string]string{
		"user":  "User",
		"count": "200",
		"page":  "2",
		"full":  "true",
	}
	params := NewListReactionsParameters()
	params.User = "User"
	params.Count = 200
	params.Page = 2
	params.Full = true
	got, paging, err := api.ListReactions(params)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("Got reaction %#v, want %#v", got, want)
		for i, item := range got {
			fmt.Printf("Item %d, Type: %s\n", i, item.Type)
			fmt.Printf("Message  %#v\n", item.Message)
			fmt.Printf("File     %#v\n", item.File)
			fmt.Printf("Comment  %#v\n", item.Comment)
			fmt.Printf("Reactions %#v\n", item.Reactions)
		}
	}
	if !reflect.DeepEqual(rh.gotParams, wantParams) {
		t.Errorf("Got params %#v, want %#v", rh.gotParams, wantParams)
	}
	if reflect.DeepEqual(paging, Paging{}) {
		t.Errorf("Want paging data, got empty struct")
	}
}
golang-github-nlopes-slack-0.1.0/rtm.go000066400000000000000000000057411311262777100177770ustar00rootroot00000000000000package slack

import (
	"encoding/json"
	"fmt"
	"net/url"
	"time"
)

// 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) {
	response := &infoResponseFull{}
	err = post("rtm.start", url.Values{"token": {api.config.token}}, response, api.debug)
	if err != nil {
		return nil, "", fmt.Errorf("post: %s", err)
	}
	if !response.Ok {
		return nil, "", response.Error
	}

	// websocket.Dial does not accept url without the port (yet)
	// Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3
	// but slack returns the address with no port, so we have to fix it
	api.Debugln("Using URL:", response.Info.URL)
	websocketURL, err = websocketizeURLPort(response.Info.URL)
	if err != nil {
		return nil, "", fmt.Errorf("parsing response URL: %s", err)
	}

	return &response.Info, websocketURL, nil
}

// 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) {
	response := &infoResponseFull{}
	err = post("rtm.connect", url.Values{"token": {api.config.token}}, response, api.debug)
	if err != nil {
		return nil, "", fmt.Errorf("post: %s", err)
	}
	if !response.Ok {
		return nil, "", response.Error
	}

	// websocket.Dial does not accept url without the port (yet)
	// Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3
	// but slack returns the address with no port, so we have to fix it
	api.Debugln("Using URL:", response.Info.URL)
	websocketURL, err = websocketizeURLPort(response.Info.URL)
	if err != nil {
		return nil, "", fmt.Errorf("parsing response URL: %s", err)
	}

	return &response.Info, websocketURL, nil
}

// 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 {
	result := &RTM{
		Client:           *api,
		IncomingEvents:   make(chan RTMEvent, 50),
		outgoingMessages: make(chan OutgoingMessage, 20),
		pings:            make(map[int]time.Time),
		isConnected:      false,
		wasIntentional:   true,
		killChannel:      make(chan bool),
		forcePing:        make(chan bool),
		rawEvents:        make(chan json.RawMessage),
		idGen:            NewSafeID(1),
	}

	if options != nil {
		result.useRTMStart = options.UseRTMStart
	} else {
		result.useRTMStart = true
	}

	return result
}
golang-github-nlopes-slack-0.1.0/search.go000066400000000000000000000067041311262777100204420ustar00rootroot00000000000000package slack

import (
	"errors"
	"net/url"
	"strconv"
)

const (
	DEFAULT_SEARCH_SORT      = "score"
	DEFAULT_SEARCH_SORT_DIR  = "desc"
	DEFAULT_SEARCH_HIGHLIGHT = false
	DEFAULT_SEARCH_COUNT     = 100
	DEFAULT_SEARCH_PAGE      = 1
)

type SearchParameters struct {
	Sort          string
	SortDirection string
	Highlight     bool
	Count         int
	Page          int
}

type CtxChannel struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

type CtxMessage struct {
	User      string `json:"user"`
	Username  string `json:"username"`
	Text      string `json:"text"`
	Timestamp string `json:"ts"`
	Type      string `json:"type"`
}

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 SearchMessages struct {
	Matches    []SearchMessage `json:"matches"`
	Paging     `json:"paging"`
	Pagination `json:"pagination"`
	Total      int `json:"total"`
}

type SearchFiles struct {
	Matches    []File `json:"matches"`
	Paging     `json:"paging"`
	Pagination `json:"pagination"`
	Total      int `json:"total"`
}

type searchResponseFull struct {
	Query          string `json:"query"`
	SearchMessages `json:"messages"`
	SearchFiles    `json:"files"`
	SlackResponse
}

func NewSearchParameters() SearchParameters {
	return SearchParameters{
		Sort:          DEFAULT_SEARCH_SORT,
		SortDirection: DEFAULT_SEARCH_SORT_DIR,
		Highlight:     DEFAULT_SEARCH_HIGHLIGHT,
		Count:         DEFAULT_SEARCH_COUNT,
		Page:          DEFAULT_SEARCH_PAGE,
	}
}

func (api *Client) _search(path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) {
	values := url.Values{
		"token": {api.config.token},
		"query": {query},
	}
	if params.Sort != DEFAULT_SEARCH_SORT {
		values.Add("sort", params.Sort)
	}
	if params.SortDirection != DEFAULT_SEARCH_SORT_DIR {
		values.Add("sort_dir", params.SortDirection)
	}
	if params.Highlight != DEFAULT_SEARCH_HIGHLIGHT {
		values.Add("highlight", strconv.Itoa(1))
	}
	if params.Count != DEFAULT_SEARCH_COUNT {
		values.Add("count", strconv.Itoa(params.Count))
	}
	if params.Page != DEFAULT_SEARCH_PAGE {
		values.Add("page", strconv.Itoa(params.Page))
	}
	response = &searchResponseFull{}
	err := post(path, values, response, api.debug)
	if err != nil {
		return nil, err
	}
	if !response.Ok {
		return nil, errors.New(response.Error)
	}
	return response, nil

}

func (api *Client) Search(query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) {
	response, err := api._search("search.all", query, params, true, true)
	if err != nil {
		return nil, nil, err
	}
	return &response.SearchMessages, &response.SearchFiles, nil
}

func (api *Client) SearchFiles(query string, params SearchParameters) (*SearchFiles, error) {
	response, err := api._search("search.files", query, params, true, false)
	if err != nil {
		return nil, err
	}
	return &response.SearchFiles, nil
}

func (api *Client) SearchMessages(query string, params SearchParameters) (*SearchMessages, error) {
	response, err := api._search("search.messages", query, params, false, true)
	if err != nil {
		return nil, err
	}
	return &response.SearchMessages, nil
}
golang-github-nlopes-slack-0.1.0/slack.go000066400000000000000000000037271311262777100202740ustar00rootroot00000000000000package slack

import (
	"errors"
	"log"
	"net/url"
	"os"
)

var logger *log.Logger // A logger that can be set by consumers
/*
  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"`
}

type AuthTestResponse struct {
	URL    string `json:"url"`
	Team   string `json:"team"`
	User   string `json:"user"`
	TeamID string `json:"team_id"`
	UserID string `json:"user_id"`
}

type authTestResponseFull struct {
	SlackResponse
	AuthTestResponse
}

type Client struct {
	config struct {
		token string
	}
	info  Info
	debug bool
}

// 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 *log.Logger) {
	logger = l
}

func New(token string) *Client {
	s := &Client{}
	s.config.token = token
	return s
}

// AuthTest tests if the user is able to do authenticated requests or not
func (api *Client) AuthTest() (response *AuthTestResponse, error error) {
	responseFull := &authTestResponseFull{}
	err := post("auth.test", url.Values{"token": {api.config.token}}, responseFull, api.debug)
	if err != nil {
		return nil, err
	}
	if !responseFull.Ok {
		return nil, errors.New(responseFull.Error)
	}
	return &responseFull.AuthTestResponse, nil
}

// SetDebug switches the api into debug mode
// When in debug mode, it logs various info about what its doing
// If you ever use this in production, don't call SetDebug(true)
func (api *Client) SetDebug(debug bool) {
	api.debug = debug
	if debug && logger == nil {
		logger = log.New(os.Stdout, "nlopes/slack", log.LstdFlags | log.Lshortfile)
	}
}

func (api *Client) Debugf(format string, v ...interface{}) {
	if api.debug {
		logger.Printf(format, v...)
	}
}

func (api *Client) Debugln(v ...interface{}) {
	if api.debug {
		logger.Println(v...)
	}
}
golang-github-nlopes-slack-0.1.0/slack_test.go000066400000000000000000000005001311262777100213150ustar00rootroot00000000000000package slack

import (
	"log"
	"net/http/httptest"
	"sync"
)

const (
	validToken = "testing-token"
)

var (
	serverAddr string
	once       sync.Once
)

func startServer() {
	server := httptest.NewServer(nil)
	serverAddr = server.Listener.Addr().String()
	log.Print("Test WebSocket server listening on ", serverAddr)
}
golang-github-nlopes-slack-0.1.0/stars.go000066400000000000000000000064131311262777100203260ustar00rootroot00000000000000package slack

import (
	"errors"
	"net/url"
	"strconv"
)

const (
	DEFAULT_STARS_USER  = ""
	DEFAULT_STARS_COUNT = 100
	DEFAULT_STARS_PAGE  = 1
)

type StarsParameters struct {
	User  string
	Count int
	Page  int
}

type StarredItem Item

type listResponseFull struct {
	Items  []Item `json:"items"`
	Paging `json:"paging"`
	SlackResponse
}

// NewStarsParameters initialises StarsParameters with default values
func NewStarsParameters() StarsParameters {
	return StarsParameters{
		User:  DEFAULT_STARS_USER,
		Count: DEFAULT_STARS_COUNT,
		Page:  DEFAULT_STARS_PAGE,
	}
}

// AddStar stars an item in a channel
func (api *Client) AddStar(channel string, item ItemRef) error {
	values := url.Values{
		"channel": {channel},
		"token":   {api.config.token},
	}
	if item.Timestamp != "" {
		values.Set("timestamp", string(item.Timestamp))
	}
	if item.File != "" {
		values.Set("file", string(item.File))
	}
	if item.Comment != "" {
		values.Set("file_comment", string(item.Comment))
	}
	response := &SlackResponse{}
	if err := post("stars.add", values, response, api.debug); err != nil {
		return err
	}
	if !response.Ok {
		return errors.New(response.Error)
	}
	return nil
}

// RemoveStar removes a starred item from a channel
func (api *Client) RemoveStar(channel string, item ItemRef) error {
	values := url.Values{
		"channel": {channel},
		"token":   {api.config.token},
	}
	if item.Timestamp != "" {
		values.Set("timestamp", string(item.Timestamp))
	}
	if item.File != "" {
		values.Set("file", string(item.File))
	}
	if item.Comment != "" {
		values.Set("file_comment", string(item.Comment))
	}
	response := &SlackResponse{}
	if err := post("stars.remove", values, response, api.debug); err != nil {
		return err
	}
	if !response.Ok {
		return errors.New(response.Error)
	}
	return nil
}

// ListStars returns information about the stars a user added
func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) {
	values := url.Values{
		"token": {api.config.token},
	}
	if params.User != DEFAULT_STARS_USER {
		values.Add("user", params.User)
	}
	if params.Count != DEFAULT_STARS_COUNT {
		values.Add("count", strconv.Itoa(params.Count))
	}
	if params.Page != DEFAULT_STARS_PAGE {
		values.Add("page", strconv.Itoa(params.Page))
	}
	response := &listResponseFull{}
	err := post("stars.list", values, response, api.debug)
	if err != nil {
		return nil, nil, err
	}
	if !response.Ok {
		return nil, nil, errors.New(response.Error)
	}
	return response.Items, &response.Paging, nil
}

// GetStarred returns a list of StarredItem items. The user then has to iterate over them and figure out what they should
// be looking at according to what is in the Type.
//    for _, item := range items {
//        switch c.Type {
//        case "file_comment":
//            log.Println(c.Comment)
//        case "file":
//             ...
//
//    }
// This function still exists to maintain backwards compatibility.
// I exposed it as returning []StarredItem, so it shall stay as StarredItem
func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, *Paging, error) {
	items, paging, err := api.ListStars(params)
	if err != nil {
		return nil, nil, err
	}
	starredItems := make([]StarredItem, len(items))
	for i, item := range items {
		starredItems[i] = StarredItem(item)
	}
	return starredItems, paging, nil
}
golang-github-nlopes-slack-0.1.0/stars_test.go000066400000000000000000000151241311262777100213640ustar00rootroot00000000000000package slack

import (
	"fmt"
	"net/http"
	"reflect"
	"testing"
)

type starsHandler struct {
	gotParams map[string]string
	response  string
}

func newStarsHandler() *starsHandler {
	return &starsHandler{
		gotParams: make(map[string]string),
		response:  `{ "ok": true }`,
	}
}

func (sh *starsHandler) accumulateFormValue(k string, r *http.Request) {
	if v := r.FormValue(k); v != "" {
		sh.gotParams[k] = v
	}
}

func (sh *starsHandler) handler(w http.ResponseWriter, r *http.Request) {
	sh.accumulateFormValue("user", r)
	sh.accumulateFormValue("count", r)
	sh.accumulateFormValue("channel", r)
	sh.accumulateFormValue("file", r)
	sh.accumulateFormValue("file_comment", r)
	sh.accumulateFormValue("page", r)
	sh.accumulateFormValue("timestamp", r)
	w.Header().Set("Content-Type", "application/json")
	w.Write([]byte(sh.response))
}

func TestSlack_AddStar(t *testing.T) {
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")
	tests := []struct {
		channel    string
		ref        ItemRef
		wantParams map[string]string
	}{
		{
			"ChannelID",
			NewRefToMessage("ChannelID", "123"),
			map[string]string{
				"channel":   "ChannelID",
				"timestamp": "123",
			},
		},
		{
			"ChannelID",
			NewRefToFile("FileID"),
			map[string]string{
				"channel": "ChannelID",
				"file":    "FileID",
			},
		},
		{
			"ChannelID",
			NewRefToComment("FileCommentID"),
			map[string]string{
				"channel":      "ChannelID",
				"file_comment": "FileCommentID",
			},
		},
	}
	var rh *starsHandler
	http.HandleFunc("/stars.add", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
	for i, test := range tests {
		rh = newStarsHandler()
		err := api.AddStar(test.channel, test.ref)
		if err != nil {
			t.Fatalf("%d: Unexpected error: %s", i, err)
		}
		if !reflect.DeepEqual(rh.gotParams, test.wantParams) {
			t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams)
		}
	}
}

func TestSlack_RemoveStar(t *testing.T) {
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")
	tests := []struct {
		channel    string
		ref        ItemRef
		wantParams map[string]string
	}{
		{
			"ChannelID",
			NewRefToMessage("ChannelID", "123"),
			map[string]string{
				"channel":   "ChannelID",
				"timestamp": "123",
			},
		},
		{
			"ChannelID",
			NewRefToFile("FileID"),
			map[string]string{
				"channel": "ChannelID",
				"file":    "FileID",
			},
		},
		{
			"ChannelID",
			NewRefToComment("FileCommentID"),
			map[string]string{
				"channel":      "ChannelID",
				"file_comment": "FileCommentID",
			},
		},
	}
	var rh *starsHandler
	http.HandleFunc("/stars.remove", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
	for i, test := range tests {
		rh = newStarsHandler()
		err := api.RemoveStar(test.channel, test.ref)
		if err != nil {
			t.Fatalf("%d: Unexpected error: %s", i, err)
		}
		if !reflect.DeepEqual(rh.gotParams, test.wantParams) {
			t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams)
		}
	}
}

func TestSlack_ListStars(t *testing.T) {
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")
	rh := newStarsHandler()
	http.HandleFunc("/stars.list", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
	rh.response = `{"ok": true,
    "items": [
        {
            "type": "message",
            "channel": "C1",
            "message": {
                "text": "hello",
                "reactions": [
                    {
                        "name": "astonished",
                        "count": 3,
                        "users": [ "U1", "U2", "U3" ]
                    },
                    {
                        "name": "clock1",
                        "count": 3,
                        "users": [ "U1", "U2" ]
                    }
                ]
            }
        },
        {
            "type": "file",
            "file": {
                "name": "toy",
                "reactions": [
                    {
                        "name": "clock1",
                        "count": 3,
                        "users": [ "U1", "U2" ]
                    }
                ]
            }
        },
        {
            "type": "file_comment",
            "file": {
                "name": "toy"
            },
            "comment": {
                "comment": "cool toy",
                "reactions": [
                    {
                        "name": "astonished",
                        "count": 3,
                        "users": [ "U1", "U2", "U3" ]
                    }
                ]
            }
        }
    ],
    "paging": {
        "count": 100,
        "total": 4,
        "page": 1,
        "pages": 1
    }}`
	want := []Item{
		NewMessageItem("C1", &Message{Msg: Msg{
			Text: "hello",
			Reactions: []ItemReaction{
				ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
				ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
			},
		}}),
		NewFileItem(&File{Name: "toy"}),
		NewFileCommentItem(&File{Name: "toy"}, &Comment{Comment: "cool toy"}),
	}
	wantStarred := make([]StarredItem, len(want))
	for i, item := range want {
		wantStarred[i] = StarredItem(item)
	}
	wantParams := map[string]string{
		"count": "200",
		"page":  "2",
	}
	params := NewStarsParameters()
	params.Count = 200
	params.Page = 2
	got, paging, err := api.ListStars(params)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("Got Stars %#v, want %#v", got, want)
		for i, item := range got {
			fmt.Printf("Item %d, Type: %s\n", i, item.Type)
			fmt.Printf("Message  %#v\n", item.Message)
			fmt.Printf("File     %#v\n", item.File)
			fmt.Printf("Comment  %#v\n", item.Comment)
		}
	}
	if !reflect.DeepEqual(rh.gotParams, wantParams) {
		t.Errorf("Got params %#v, want %#v", rh.gotParams, wantParams)
	}
	if reflect.DeepEqual(paging, Paging{}) {
		t.Errorf("Want paging data, got empty struct")
	}
	// Test GetStarred
	gotStarred, paging, err := api.GetStarred(params)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}
	if !reflect.DeepEqual(gotStarred, wantStarred) {
		t.Errorf("Got Stars %#v, want %#v", gotStarred, wantStarred)
		for i, item := range got {
			fmt.Printf("Item %d, Type: %s\n", i, item.Type)
			fmt.Printf("Message  %#v\n", item.Message)
			fmt.Printf("File     %#v\n", item.File)
			fmt.Printf("Comment  %#v\n", item.Comment)
		}
	}
	if !reflect.DeepEqual(rh.gotParams, wantParams) {
		t.Errorf("Got params %#v, want %#v", rh.gotParams, wantParams)
	}
	if reflect.DeepEqual(paging, Paging{}) {
		t.Errorf("Want paging data, got empty struct")
	}
}
golang-github-nlopes-slack-0.1.0/team.go000066400000000000000000000075161311262777100201250ustar00rootroot00000000000000package slack

import (
	"errors"
	"net/url"
	"strconv"
)

const (
	DEFAULT_LOGINS_COUNT   = 100
	DEFAULT_LOGINS_PAGE    = 1
)

type TeamResponse struct {
	Team TeamInfo `json:"team"`
	SlackResponse
}

type TeamInfo struct {
	ID          string                 `json:"id"`
	Name        string                 `json:"name"`
	Domain      string                 `json:"domain"`
	EmailDomain string                 `json:"email_domain"`
	Icon        map[string]interface{} `json:"icon"`
}

type LoginResponse struct {
	Logins []Login `json:"logins"`
	Paging         `json:"paging"`
	SlackResponse
}


type Login struct {
	UserID    string `json:"user_id"`
	Username  string `json:"username"`
	DateFirst int    `json:"date_first"`
	DateLast  int    `json:"date_last"`
	Count     int    `json:"count"`
	IP        string `json:"ip"`
	UserAgent string `json:"user_agent"`
	ISP       string `json:"isp"`
	Country   string `json:"country"`
	Region    string `json:"region"`
}

type BillableInfoResponse struct {
	BillableInfo map[string]BillingActive `json:"billable_info"`
	SlackResponse

}

type BillingActive struct {
	BillingActive bool `json:"billing_active"`
}

// AccessLogParameters contains all the parameters necessary (including the optional ones) for a GetAccessLogs() request
type AccessLogParameters struct {
	Count         int
	Page          int
}

// NewAccessLogParameters provides an instance of AccessLogParameters with all the sane default values set
func NewAccessLogParameters() AccessLogParameters {
	return AccessLogParameters{
		Count: DEFAULT_LOGINS_COUNT,
		Page:  DEFAULT_LOGINS_PAGE,
	}
}


func teamRequest(path string, values url.Values, debug bool) (*TeamResponse, error) {
	response := &TeamResponse{}
	err := post(path, values, response, debug)
	if err != nil {
		return nil, err
	}

	if !response.Ok {
		return nil, errors.New(response.Error)
	}

	return response, nil
}

func billableInfoRequest(path string, values url.Values, debug bool) (map[string]BillingActive, error) {
	response := &BillableInfoResponse{}
	err := post(path, values, response, debug)
	if err != nil {
		return nil, err
	}

	if !response.Ok {
		return nil, errors.New(response.Error)
	}

	return response.BillableInfo, nil
}

func accessLogsRequest(path string, values url.Values, debug bool) (*LoginResponse, error) {
	response := &LoginResponse{}
	err := post(path, values, response, debug)
	if err != nil {
		return nil, err
	}
	if !response.Ok {
		return nil, errors.New(response.Error)
	}
	return response, nil
}


// GetTeamInfo gets the Team Information of the user
func (api *Client) GetTeamInfo() (*TeamInfo, error) {
	values := url.Values{
		"token": {api.config.token},
	}

	response, err := teamRequest("team.info", values, api.debug)
	if err != nil {
		return nil, err
	}
	return &response.Team, nil
}

// GetAccessLogs retrieves a page of logins according to the parameters given
func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging, error) {
	values := url.Values{
		"token": {api.config.token},
	}
	if params.Count != DEFAULT_LOGINS_COUNT {
		values.Add("count", strconv.Itoa(params.Count))
	}
	if params.Page != DEFAULT_LOGINS_PAGE {
		values.Add("page", strconv.Itoa(params.Page))
	}
	response, err := accessLogsRequest("team.accessLogs", values, api.debug)
	if err != nil {
		return nil, nil, err
	}
	return response.Logins, &response.Paging, nil
}

func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error) {
	values := url.Values{
		"token": {api.config.token},
		"user": {user},
	}

	return billableInfoRequest("team.billableInfo", values, api.debug)
}

// GetBillableInfoForTeam returns the billing_active status of all users on the team.
func (api *Client) GetBillableInfoForTeam() (map[string]BillingActive, error) {
	values := url.Values{
		"token": {api.config.token},
	}

	return billableInfoRequest("team.billableInfo", values, api.debug)
}
golang-github-nlopes-slack-0.1.0/team_test.go000066400000000000000000000102551311262777100211560ustar00rootroot00000000000000package slack

import (
	"errors"
	"net/http"
	"testing"
	"strings"
)

var (
	ErrIncorrectResponse = errors.New("Response is incorrect")
)

func getTeamInfo(rw http.ResponseWriter, r *http.Request) {
	rw.Header().Set("Content-Type", "application/json")
	response := []byte(`{"ok": true, "team": {
			"id": "F0UWHUX",
			"name": "notalar",
			"domain": "notalar",
			"icon": {
              "image_34": "https://slack.global.ssl.fastly.net/66f9/img/avatars-teams/ava_0002-34.png",
              "image_44": "https://slack.global.ssl.fastly.net/66f9/img/avatars-teams/ava_0002-44.png",
              "image_55": "https://slack.global.ssl.fastly.net/66f9/img/avatars-teams/ava_0002-55.png",
              "image_default": true
          }
		}}`)
	rw.Write(response)
}

func TestGetTeamInfo(t *testing.T) {
	http.HandleFunc("/team.info", getTeamInfo)

	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")

	teamInfo, err := api.GetTeamInfo()
	if err != nil {
		t.Errorf("Unexpected error: %s", err)
		return
	}

	// t.Fatal refers to -> t.Errorf & return
	if teamInfo.ID != "F0UWHUX" {
		t.Fatal(ErrIncorrectResponse)
	}
	if teamInfo.Domain != "notalar" {
		t.Fatal(ErrIncorrectResponse)
	}
	if teamInfo.Name != "notalar" {
		t.Fatal(ErrIncorrectResponse)
	}
	if teamInfo.Icon == nil {
		t.Fatal(ErrIncorrectResponse)
	}
}

func getTeamAccessLogs(rw http.ResponseWriter, r *http.Request) {
	rw.Header().Set("Content-Type", "application/json")
	response := []byte(`{"ok": true, "logins": [{
			"user_id": "F0UWHUX",
			"username": "notalar",
			"date_first": 1475684477,
			"date_last": 1475684645,
			"count": 8,
			"ip": "127.0.0.1",
			"user_agent": "SlackWeb/3abb0ae2380d48a9ae20c58cc624ebcd Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Slack/1.2.6 Chrome/45.0.2454.85 AtomShell/0.34.3 Safari/537.36 Slack_SSB/1.2.6",
			"isp": "AT&T U-verse",
                        "country": "US",
                        "region": "IN"
                        },
                        {
                        "user_id": "XUHWU0F",
			"username": "ralaton",
			"date_first": 1447395893,
			"date_last": 1447395965,
			"count": 5,
			"ip": "192.168.0.1",
			"user_agent": "com.tinyspeck.chatlyio/2.60 (iPhone; iOS 9.1; Scale/3.00)",
			"isp": null,
                        "country": null,
                        "region": null
                        }],
                        "paging": {
    			"count": 2,
    			"total": 2,
    			"page": 1,
    			"pages": 1
    			}
  }`)
	rw.Write(response)
}

func TestGetAccessLogs(t *testing.T) {
	http.HandleFunc("/team.accessLogs", getTeamAccessLogs)

	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")

	logins, paging, err := api.GetAccessLogs(NewAccessLogParameters())
	if err != nil {
		t.Errorf("Unexpected error: %s", err)
		return
	}

	if len(logins) != 2 {
		t.Fatal("Should have been 2 logins")
	}

	// test the first login
	login1 := logins[0]
	login2 := logins[1]

	if (login1.UserID != "F0UWHUX") {
		t.Fatal(ErrIncorrectResponse)
	}
	if (login1.Username != "notalar") {
		t.Fatal(ErrIncorrectResponse)
	}
	if (login1.DateFirst != 1475684477) {
		t.Fatal(ErrIncorrectResponse)
	}
	if (login1.DateLast != 1475684645) {
		t.Fatal(ErrIncorrectResponse)
	}
	if (login1.Count != 8) {
		t.Fatal(ErrIncorrectResponse)
	}
	if (login1.IP != "127.0.0.1") {
		t.Fatal(ErrIncorrectResponse)
	}
	if (!strings.HasPrefix(login1.UserAgent, "SlackWeb")) {
		t.Fatal(ErrIncorrectResponse)
	}
	if (login1.ISP != "AT&T U-verse") {
		t.Fatal(ErrIncorrectResponse)
	}
	if (login1.Country != "US") {
		t.Fatal(ErrIncorrectResponse)
	}
	if (login1.Region != "IN") {
		t.Fatal(ErrIncorrectResponse)
	}

	// test that the null values from login2 are coming across correctly
	if (login2.ISP != "") {
		t.Fatal(ErrIncorrectResponse)
	}
	if (login2.Country != "") {
		t.Fatal(ErrIncorrectResponse)
	}
	if (login2.Region != "") {
		t.Fatal(ErrIncorrectResponse)
	}

	// test the paging
	if (paging.Count != 2) {
		t.Fatal(ErrIncorrectResponse)
	}
	if (paging.Total != 2) {
		t.Fatal(ErrIncorrectResponse)
	}
	if (paging.Page != 1) {
		t.Fatal(ErrIncorrectResponse)
	}
	if (paging.Pages != 1) {
		t.Fatal(ErrIncorrectResponse)
	}
}

golang-github-nlopes-slack-0.1.0/usergroups.go000066400000000000000000000112351311262777100214060ustar00rootroot00000000000000package slack

import (
	"errors"
	"net/url"
	"strings"
)

// UserGroup contains all the information of a user group
type UserGroup struct {
	ID          string         `json:"id"`
	TeamID      string         `json:"team_id"`
	IsUserGroup bool           `json:"is_usergroup"`
	Name        string         `json:"name"`
	Description string         `json:"description"`
	Handle      string         `json:"handle"`
	IsExternal  bool           `json:"is_external"`
	DateCreate  JSONTime       `json:"date_create"`
	DateUpdate  JSONTime       `json:"date_update"`
	DateDelete  JSONTime       `json:"date_delete"`
	AutoType    string         `json:"auto_type"`
	CreatedBy   string         `json:"created_by"`
	UpdatedBy   string         `json:"updated_by"`
	DeletedBy   string         `json:"deleted_by"`
	Prefs       UserGroupPrefs `json:"prefs"`
	UserCount   int            `json:"user_count"`
}

// UserGroupPrefs contains default channels and groups (private channels)
type UserGroupPrefs struct {
	Channels []string `json:"channels"`
	Groups   []string `json:"groups"`
}

type userGroupResponseFull struct {
	UserGroups []UserGroup `json:"usergroups"`
	UserGroup  UserGroup   `json:"usergroup"`
	Users      []string    `json:"users"`
	SlackResponse
}

func userGroupRequest(path string, values url.Values, debug bool) (*userGroupResponseFull, error) {
	response := &userGroupResponseFull{}
	err := post(path, values, response, debug)
	if err != nil {
		return nil, err
	}
	if !response.Ok {
		return nil, errors.New(response.Error)
	}
	return response, nil
}

// CreateUserGroup creates a new user group
func (api *Client) CreateUserGroup(userGroup UserGroup) (UserGroup, error) {
	values := url.Values{
		"token": {api.config.token},
		"name":  {userGroup.Name},
	}

	if userGroup.Handle != "" {
		values["handle"] = []string{userGroup.Handle}
	}

	if userGroup.Description != "" {
		values["description"] = []string{userGroup.Description}
	}

	if len(userGroup.Prefs.Channels) > 0 {
		values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")}
	}

	response, err := userGroupRequest("usergroups.create", values, api.debug)
	if err != nil {
		return UserGroup{}, err
	}
	return response.UserGroup, nil
}

// DisableUserGroup disables an existing user group
func (api *Client) DisableUserGroup(userGroup string) (UserGroup, error) {
	values := url.Values{
		"token":     {api.config.token},
		"usergroup": {userGroup},
	}

	response, err := userGroupRequest("usergroups.disable", values, api.debug)
	if err != nil {
		return UserGroup{}, err
	}
	return response.UserGroup, nil
}

// EnableUserGroup enables an existing user group
func (api *Client) EnableUserGroup(userGroup string) (UserGroup, error) {
	values := url.Values{
		"token":     {api.config.token},
		"usergroup": {userGroup},
	}

	response, err := userGroupRequest("usergroups.enable", values, api.debug)
	if err != nil {
		return UserGroup{}, err
	}
	return response.UserGroup, nil
}

// GetUserGroups returns a list of user groups for the team
func (api *Client) GetUserGroups() ([]UserGroup, error) {
	values := url.Values{
		"token": {api.config.token},
	}

	response, err := userGroupRequest("usergroups.list", values, api.debug)
	if err != nil {
		return nil, err
	}
	return response.UserGroups, nil
}

// UpdateUserGroup will update an existing user group
func (api *Client) UpdateUserGroup(userGroup UserGroup) (UserGroup, error) {
	values := url.Values{
		"token":     {api.config.token},
		"usergroup": {userGroup.ID},
	}

	if userGroup.Name != "" {
		values["name"] = []string{userGroup.Name}
	}

	if userGroup.Handle != "" {
		values["handle"] = []string{userGroup.Handle}
	}

	if userGroup.Description != "" {
		values["description"] = []string{userGroup.Description}
	}

	response, err := userGroupRequest("usergroups.update", values, api.debug)
	if err != nil {
		return UserGroup{}, err
	}
	return response.UserGroup, nil
}

// GetUserGroupMembers will retrieve the current list of users in a group
func (api *Client) GetUserGroupMembers(userGroup string) ([]string, error) {
	values := url.Values{
		"token":     {api.config.token},
		"usergroup": {userGroup},
	}

	response, err := userGroupRequest("usergroups.users.list", values, api.debug)
	if err != nil {
		return []string{}, err
	}
	return response.Users, nil
}

// UpdateUserGroupMembers will update the members of an existing user group
func (api *Client) UpdateUserGroupMembers(userGroup string, members string) (UserGroup, error) {
	values := url.Values{
		"token":     {api.config.token},
		"usergroup": {userGroup},
		"users":     {members},
	}

	response, err := userGroupRequest("usergroups.users.update", values, api.debug)
	if err != nil {
		return UserGroup{}, err
	}
	return response.UserGroup, nil
}
golang-github-nlopes-slack-0.1.0/usergroups_test.go000066400000000000000000000106531311262777100224500ustar00rootroot00000000000000package slack

import (
	"net/http"
	"reflect"
	"testing"
)

type userGroupsHandler struct {
	gotParams map[string]string
	response  string
}

func newUserGroupsHandler() *userGroupsHandler {
	return &userGroupsHandler{
		gotParams: make(map[string]string),
		response: `{
    "ok": true,
    "usergroup": {
        "id": "S0615G0KT",
        "team_id": "T060RNRCH",
        "is_usergroup": true,
        "name": "Marketing Team",
        "description": "Marketing gurus, PR experts and product advocates.",
        "handle": "marketing-team",
        "is_external": false,
        "date_create": 1446746793,
        "date_update": 1446746793,
        "date_delete": 0,
        "auto_type": null,
        "created_by": "U060RNRCZ",
        "updated_by": "U060RNRCZ",
        "deleted_by": null,
        "prefs": {
            "channels": [

            ],
            "groups": [

            ]
        },
        "user_count": 0
    }
}`,
	}
}

func (ugh *userGroupsHandler) accumulateFormValue(k string, r *http.Request) {
	if v := r.FormValue(k); v != "" {
		ugh.gotParams[k] = v
	}
}

func (ugh *userGroupsHandler) handler(w http.ResponseWriter, r *http.Request) {
	ugh.accumulateFormValue("name", r)
	ugh.accumulateFormValue("description", r)
	ugh.accumulateFormValue("handle", r)
	w.Header().Set("Content-Type", "application/json")
	w.Write([]byte(ugh.response))
}

func TestCreateUserGroup(t *testing.T) {
	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")

	tests := []struct {
		userGroup  UserGroup
		wantParams map[string]string
	}{
		{
			UserGroup{
				Name:        "Marketing Team",
				Description: "Marketing gurus, PR experts and product advocates.",
				Handle:      "marketing-team"},
			map[string]string{
				"name":        "Marketing Team",
				"description": "Marketing gurus, PR experts and product advocates.",
				"handle":      "marketing-team",
			},
		},
	}

	var rh *userGroupsHandler
	http.HandleFunc("/usergroups.create", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })

	for i, test := range tests {
		rh = newUserGroupsHandler()
		_, err := api.CreateUserGroup(test.userGroup)
		if err != nil {
			t.Fatalf("%d: Unexpected error: %s", i, err)
		}
		if !reflect.DeepEqual(rh.gotParams, test.wantParams) {
			t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams)
		}
	}
}

func getUserGroups(rw http.ResponseWriter, r *http.Request) {
	rw.Header().Set("Content-Type", "application/json")
	response := []byte(`{
    "ok": true,
    "usergroups": [
        {
            "id": "S0614TZR7",
            "team_id": "T060RNRCH",
            "is_usergroup": true,
            "name": "Team Admins",
            "description": "A group of all Administrators on your team.",
            "handle": "admins",
            "is_external": false,
            "date_create": 1446598059,
            "date_update": 1446670362,
            "date_delete": 0,
            "auto_type": "admin",
            "created_by": "USLACKBOT",
            "updated_by": "U060RNRCZ",
            "deleted_by": null,
            "prefs": {
                "channels": [
                  "channel1",
                  "channel2"
                ],
                "groups": [
                  "group1",
                  "group2",
                  "group3"
                ]
            },
            "user_count": 2
        }
    ]
}`)
	rw.Write(response)
}

func TestGetUserGroups(t *testing.T) {
	http.HandleFunc("/usergroups.list", getUserGroups)

	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")

	userGroups, err := api.GetUserGroups()
	if err != nil {
		t.Errorf("Unexpected error: %s", err)
		return
	}

	// t.Fatal refers to -> t.Errorf & return
	if len(userGroups) != 1 {
		t.Fatal(ErrIncorrectResponse)
	}

	S0614TZR7 := UserGroup{
		ID:          "S0614TZR7",
		TeamID:      "T060RNRCH",
		IsUserGroup: true,
		Name:        "Team Admins",
		Description: "A group of all Administrators on your team.",
		Handle:      "admins",
		IsExternal:  false,
		DateCreate:  1446598059,
		DateUpdate:  1446670362,
		DateDelete:  0,
		AutoType:    "admin",
		CreatedBy:   "USLACKBOT",
		UpdatedBy:   "U060RNRCZ",
		DeletedBy:   "",
		Prefs: UserGroupPrefs{
			Channels: []string{"channel1", "channel2"},
			Groups:   []string{"group1", "group2", "group3"},
		},
		UserCount: 2,
	}

	if !reflect.DeepEqual(userGroups[0], S0614TZR7) {
		t.Errorf("Got %#v, want %#v", userGroups[0], S0614TZR7)
	}
}
golang-github-nlopes-slack-0.1.0/users.go000066400000000000000000000217551311262777100203410ustar00rootroot00000000000000package slack

import (
	"encoding/json"
	"errors"
	"net/url"
)

const (
	DEFAULT_USER_PHOTO_CROP_X = -1
	DEFAULT_USER_PHOTO_CROP_Y = -1
	DEFAULT_USER_PHOTO_CROP_W = -1
)

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

// User contains all the information of a user
type User struct {
	ID                string      `json:"id"`
	Name              string      `json:"name"`
	Deleted           bool        `json:"deleted"`
	Color             string      `json:"color"`
	RealName          string      `json:"real_name"`
	TZ                string      `json:"tz,omitempty"`
	TZLabel           string      `json:"tz_label"`
	TZOffset          int         `json:"tz_offset"`
	Profile           UserProfile `json:"profile"`
	IsBot             bool        `json:"is_bot"`
	IsAdmin           bool        `json:"is_admin"`
	IsOwner           bool        `json:"is_owner"`
	IsPrimaryOwner    bool        `json:"is_primary_owner"`
	IsRestricted      bool        `json:"is_restricted"`
	IsUltraRestricted bool        `json:"is_ultra_restricted"`
	Has2FA            bool        `json:"has_2fa"`
	HasFiles          bool        `json:"has_files"`
	Presence          string      `json:"presence"`
}

// UserPresence contains details about a user online status
type UserPresence struct {
	Presence        string   `json:"presence,omitempty"`
	Online          bool     `json:"online,omitempty"`
	AutoAway        bool     `json:"auto_away,omitempty"`
	ManualAway      bool     `json:"manual_away,omitempty"`
	ConnectionCount int      `json:"connection_count,omitempty"`
	LastActivity    JSONTime `json:"last_activity,omitempty"`
}

type UserIdentityResponse struct {
	User UserIdentity `json:"user"`
	Team TeamIdentity `json:"team"`
	SlackResponse
}

type UserIdentity struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	Email    string `json:"email"`
	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"`
	Image512 string `json:"image_512"`
}

type TeamIdentity struct {
	ID            string `json:"id"`
	Name          string `json:"name"`
	Domain        string `json:"domain"`
	Image34       string `json:"image_34"`
	Image44       string `json:"image_44"`
	Image68       string `json:"image_68"`
	Image88       string `json:"image_88"`
	Image102      string `json:"image_102"`
	Image132      string `json:"image_132"`
	Image230      string `json:"image_230"`
	ImageDefault  bool   `json:"image_default"`
	ImageOriginal string `json:"image_original"`
}

type userResponseFull struct {
	Members      []User                  `json:"members,omitempty"` // ListUsers
	User         `json:"user,omitempty"` // GetUserInfo
	UserPresence                         // GetUserPresence
	SlackResponse
}

type UserSetPhotoParams struct {
	CropX int
	CropY int
	CropW int
}

func NewUserSetPhotoParams() UserSetPhotoParams {
	return UserSetPhotoParams{
		CropX: DEFAULT_USER_PHOTO_CROP_X,
		CropY: DEFAULT_USER_PHOTO_CROP_Y,
		CropW: DEFAULT_USER_PHOTO_CROP_W,
	}
}

func userRequest(path string, values url.Values, debug bool) (*userResponseFull, error) {
	response := &userResponseFull{}
	err := post(path, values, response, debug)
	if err != nil {
		return nil, err
	}
	if !response.Ok {
		return nil, errors.New(response.Error)
	}
	return response, nil
}

// GetUserPresence will retrieve the current presence status of given user.
func (api *Client) GetUserPresence(user string) (*UserPresence, error) {
	values := url.Values{
		"token": {api.config.token},
		"user":  {user},
	}
	response, err := userRequest("users.getPresence", values, api.debug)
	if err != nil {
		return nil, err
	}
	return &response.UserPresence, nil
}

// GetUserInfo will retrieve the complete user information
func (api *Client) GetUserInfo(user string) (*User, error) {
	values := url.Values{
		"token": {api.config.token},
		"user":  {user},
	}
	response, err := userRequest("users.info", values, api.debug)
	if err != nil {
		return nil, err
	}
	return &response.User, nil
}

// GetUsers returns the list of users (with their detailed information)
func (api *Client) GetUsers() ([]User, error) {
	values := url.Values{
		"token":    {api.config.token},
		"presence": {"1"},
	}
	response, err := userRequest("users.list", values, api.debug)
	if err != nil {
		return nil, err
	}
	return response.Members, nil
}

// SetUserAsActive marks the currently authenticated user as active
func (api *Client) SetUserAsActive() error {
	values := url.Values{
		"token": {api.config.token},
	}
	_, err := userRequest("users.setActive", values, api.debug)
	if err != nil {
		return err
	}
	return nil
}

// SetUserPresence changes the currently authenticated user presence
func (api *Client) SetUserPresence(presence string) error {
	values := url.Values{
		"token":    {api.config.token},
		"presence": {presence},
	}
	_, err := userRequest("users.setPresence", values, api.debug)
	if err != nil {
		return err
	}
	return nil

}

// GetUserIdentity will retrieve user info available per identity scopes
func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) {
	values := url.Values{
		"token": {api.config.token},
	}
	response := &UserIdentityResponse{}
	err := post("users.identity", values, response, api.debug)
	if err != nil {
		return nil, err
	}
	if !response.Ok {
		return nil, errors.New(response.Error)
	}
	return response, nil
}

// SetUserPhoto changes the currently authenticated user's profile image
func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error {
	response := &SlackResponse{}
	values := url.Values{
		"token": {api.config.token},
	}
	if params.CropX != DEFAULT_USER_PHOTO_CROP_X {
		values.Add("crop_x", string(params.CropX))
	}
	if params.CropY != DEFAULT_USER_PHOTO_CROP_Y {
		values.Add("crop_y", string(params.CropY))
	}
	if params.CropW != DEFAULT_USER_PHOTO_CROP_W {
		values.Add("crop_w", string(params.CropW))
	}
	err := postLocalWithMultipartResponse("users.setPhoto", image, "image", values, response, api.debug)
	if err != nil {
		return err
	}
	if !response.Ok {
		return errors.New(response.Error)
	}
	return nil
}

// DeleteUserPhoto deletes the current authenticated user's profile image
func (api *Client) DeleteUserPhoto() error {
	response := &SlackResponse{}
	values := url.Values{
		"token": {api.config.token},
	}
	err := post("users.deletePhoto", values, response, api.debug)
	if err != nil {
		return err
	}
	if !response.Ok {
		return errors.New(response.Error)
	}
	return nil
}

// SetUserCustomStatus will set a custom status and emoji for the currently
// authenticated user. If statusEmoji is "" and statusText is not, the Slack API
// will automatically set it to ":speech_balloon:". Otherwise, if both are ""
// the Slack API will unset the custom status/emoji.
func (api *Client) SetUserCustomStatus(statusText, statusEmoji string) error {
	// XXX(theckman): this anonymous struct is for making requests to the Slack
	// API for setting and unsetting a User's Custom Status/Emoji. To change
	// these values we must provide a JSON document as the profile POST field.
	//
	// We use an anonymous struct over UserProfile because to unset the values
	// on the User's profile we cannot use the `json:"omitempty"` tag. This is
	// because an empty string ("") is what's used to unset the values. Check
	// out the API docs for more details:
	//
	// - https://api.slack.com/docs/presence-and-status#custom_status
	profile, err := json.Marshal(
		&struct {
			StatusText  string `json:"status_text"`
			StatusEmoji string `json:"status_emoji"`
		}{
			StatusText:  statusText,
			StatusEmoji: statusEmoji,
		},
	)

	if err != nil {
		return err
	}

	values := url.Values{
		"token":   {api.config.token},
		"profile": {string(profile)},
	}

	response := &userResponseFull{}

	if err = post("users.profile.set", values, response, api.debug); err != nil {
		return err
	}

	if !response.Ok {
		return errors.New(response.Error)
	}

	return nil
}

// UnsetUserCustomStatus removes the custom status message for the currently
// authenticated user. This is a convenience method that wraps
// (*Client).SetUserCustomStatus().
func (api *Client) UnsetUserCustomStatus() error {
	return api.SetUserCustomStatus("", "")
}
golang-github-nlopes-slack-0.1.0/users_test.go000066400000000000000000000132051311262777100213670ustar00rootroot00000000000000package slack

import (
	"encoding/json"
	"fmt"
	"net/http"
	"testing"
)

func getUserIdentity(rw http.ResponseWriter, r *http.Request) {
	rw.Header().Set("Content-Type", "application/json")
	response := []byte(`{
  "ok": true,
  "user": {
    "id": "UXXXXXXXX",
    "name": "Test User",
    "email": "test@test.com",
    "image_24": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_24.jpg",
    "image_32": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_32.jpg",
    "image_48": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_48.jpg",
    "image_72": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_72.jpg",
    "image_192": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_192.jpg",
    "image_512": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_512.jpg"
  },
  "team": {
    "id": "TXXXXXXXX",
    "name": "team-name",
    "domain": "team-domain",
    "image_34": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_34.jpg",
    "image_44": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_44.jpg",
    "image_68": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_68.jpg",
    "image_88": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_88.jpg",
    "image_102": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_102.jpg",
    "image_132": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_132.jpg",
    "image_230": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_230.jpg",
    "image_original": "https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-10-18\/92962080834_ef14c1469fc0741caea1_original.jpg"
  }
}`)
	rw.Write(response)
}

func httpTestErrReply(w http.ResponseWriter, clientErr bool, msg string) {
	if clientErr {
		w.WriteHeader(http.StatusBadRequest)
	} else {
		w.WriteHeader(http.StatusInternalServerError)
	}

	w.Header().Set("Content-Type", "application/json")

	body, _ := json.Marshal(&SlackResponse{
		Ok: false, Error: msg,
	})

	w.Write(body)
}

func newProfileHandler(up *UserProfile) (setter func(http.ResponseWriter, *http.Request)) {
	return func(w http.ResponseWriter, r *http.Request) {
		if up == nil {
			httpTestErrReply(w, false, "err: UserProfile is nil")
			return
		}

		if err := r.ParseForm(); err != nil {
			httpTestErrReply(w, true, fmt.Sprintf("err parsing form: %s", err.Error()))
			return
		}

		values := r.Form

		if len(values["profile"]) == 0 {
			httpTestErrReply(w, true, `POST data must include a "profile" field`)
			return
		}

		profile := []byte(values["profile"][0])

		userProfile := UserProfile{}

		if err := json.Unmarshal(profile, &userProfile); err != nil {
			httpTestErrReply(w, true, fmt.Sprintf("err parsing JSON: %s\n\njson: `%s`", err.Error(), profile))
			return
		}

		*up = userProfile

		// TODO(theckman): enhance this to return a full User object
		fmt.Fprint(w, `{"ok":true}`)
	}
}

func TestGetUserIdentity(t *testing.T) {
	http.HandleFunc("/users.identity", getUserIdentity)

	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")

	identity, err := api.GetUserIdentity()
	if err != nil {
		t.Errorf("Unexpected error: %s", err)
		return
	}

	// t.Fatal refers to -> t.Errorf & return
	if identity.User.ID != "UXXXXXXXX" {
		t.Fatal(ErrIncorrectResponse)
	}
	if identity.User.Name != "Test User" {
		t.Fatal(ErrIncorrectResponse)
	}
	if identity.User.Email != "test@test.com" {
		t.Fatal(ErrIncorrectResponse)
	}
	if identity.Team.ID != "TXXXXXXXX" {
		t.Fatal(ErrIncorrectResponse)
	}
	if identity.Team.Name != "team-name" {
		t.Fatal(ErrIncorrectResponse)
	}
	if identity.Team.Domain != "team-domain" {
		t.Fatal(ErrIncorrectResponse)
	}
	if identity.User.Image24 == "" {
		t.Fatal(ErrIncorrectResponse)
	}
	if identity.Team.Image34 == "" {
		t.Fatal(ErrIncorrectResponse)
	}
}

func TestUserCustomStatus(t *testing.T) {
	up := &UserProfile{}

	setUserProfile := newProfileHandler(up)

	http.HandleFunc("/users.profile.set", setUserProfile)

	once.Do(startServer)
	SLACK_API = "http://" + serverAddr + "/"
	api := New("testing-token")

	testSetUserCustomStatus(api, up, t)
	testUnsetUserCustomStatus(api, up, t)
}

func testSetUserCustomStatus(api *Client, up *UserProfile, t *testing.T) {
	const (
		statusText  = "testStatus"
		statusEmoji = ":construction:"
	)

	if err := api.SetUserCustomStatus(statusText, statusEmoji); err != nil {
		t.Fatalf(`SetUserCustomStatus(%q, %q) = %#v, want `, statusText, statusEmoji, err)
	}

	if up.StatusText != statusText {
		t.Fatalf(`UserProfile.StatusText = %q, want %q`, up.StatusText, statusText)
	}

	if up.StatusEmoji != statusEmoji {
		t.Fatalf(`UserProfile.StatusEmoji = %q, want %q`, up.StatusEmoji, statusEmoji)
	}
}

func testUnsetUserCustomStatus(api *Client, up *UserProfile, t *testing.T) {
	if err := api.UnsetUserCustomStatus(); err != nil {
		t.Fatalf(`UnsetUserCustomStatus() = %#v, want `, err)
	}

	if up.StatusText != "" {
		t.Fatalf(`UserProfile.StatusText = %q, want %q`, up.StatusText, "")
	}

	if up.StatusEmoji != "" {
		t.Fatalf(`UserProfile.StatusEmoji = %q, want %q`, up.StatusEmoji, "")
	}
}
golang-github-nlopes-slack-0.1.0/websocket.go000066400000000000000000000052631311262777100211620ustar00rootroot00000000000000package slack

import (
	"encoding/json"
	"errors"
	"time"

	"golang.org/x/net/websocket"
)

const (
	// MaxMessageTextLength is the current maximum message length in number of characters as defined here
	// https://api.slack.com/rtm#limits
	MaxMessageTextLength = 4000
)

// RTM represents a managed websocket connection. It also supports
// all the methods of the `Client` type.
//
// Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions)
type RTM struct {
	idGen IDGenerator
	pings map[int]time.Time

	// Connection life-cycle
	conn             *websocket.Conn
	IncomingEvents   chan RTMEvent
	outgoingMessages chan OutgoingMessage
	killChannel      chan bool
	forcePing        chan bool
	rawEvents        chan json.RawMessage
	wasIntentional   bool
	isConnected      bool

	// Client is the main API, embedded
	Client
	websocketURL string

	// UserDetails upon connection
	info *Info

	// useRTMStart should be set to true if you want to use
	// rtm.start to connect to Slack, otherwise it will use
	// rtm.connect
	useRTMStart bool
}

// RTMOptions allows configuration of various options available for RTM messaging
//
// This structure will evolve in time so please make sure you are always using the
// named keys for every entry available as per Go 1 compatibility promise adding fields
// to this structure should not be considered a breaking change.
type RTMOptions struct {
	// UseRTMStart set to true in order to use rtm.start or false to use rtm.connect
	// 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
	UseRTMStart bool
}

// Disconnect and wait, blocking until a successful disconnection.
func (rtm *RTM) Disconnect() error {
	if !rtm.isConnected {
		return errors.New("Invalid call to Disconnect - Slack API is already disconnected")
	}
	rtm.killChannel <- true
	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
// StartRTM().
func (rtm *RTM) GetInfo() *Info {
	return rtm.info
}

// SendMessage submits a simple message through the websocket.  For
// more complicated messages, use `rtm.PostMessage` with a complete
// struct describing your attachments and all.
func (rtm *RTM) SendMessage(msg *OutgoingMessage) {
	if msg == nil {
		rtm.Debugln("Error: Attempted to SendMessage(nil)")
		return
	}

	rtm.outgoingMessages <- *msg
}
golang-github-nlopes-slack-0.1.0/websocket_channels.go000066400000000000000000000043241311262777100230320ustar00rootroot00000000000000package slack

// ChannelCreatedEvent represents the Channel created event
type ChannelCreatedEvent struct {
	Type           string             `json:"type"`
	Channel        ChannelCreatedInfo `json:"channel"`
	EventTimestamp string             `json:"event_ts"`
}

// ChannelCreatedInfo represents the information associated with the Channel created event
type ChannelCreatedInfo struct {
	ID        string `json:"id"`
	IsChannel bool   `json:"is_channel"`
	Name      string `json:"name"`
	Created   int    `json:"created"`
	Creator   string `json:"creator"`
}

// ChannelJoinedEvent represents the Channel joined event
type ChannelJoinedEvent struct {
	Type    string  `json:"type"`
	Channel Channel `json:"channel"`
}

// ChannelInfoEvent represents the Channel info event
type ChannelInfoEvent struct {
	// channel_left
	// channel_deleted
	// channel_archive
	// channel_unarchive
	Type      string `json:"type"`
	Channel   string `json:"channel"`
	User      string `json:"user,omitempty"`
	Timestamp string `json:"ts,omitempty"`
}

// ChannelRenameEvent represents the Channel rename event
type ChannelRenameEvent struct {
	Type      string            `json:"type"`
	Channel   ChannelRenameInfo `json:"channel"`
	Timestamp string            `json:"event_ts"`
}

// ChannelRenameInfo represents the information associated with a Channel rename event
type ChannelRenameInfo struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Created string `json:"created"`
}

// ChannelHistoryChangedEvent represents the Channel history changed event
type ChannelHistoryChangedEvent struct {
	Type           string `json:"type"`
	Latest         string `json:"latest"`
	Timestamp      string `json:"ts"`
	EventTimestamp string `json:"event_ts"`
}

// ChannelMarkedEvent represents the Channel marked event
type ChannelMarkedEvent ChannelInfoEvent

// ChannelLeftEvent represents the Channel left event
type ChannelLeftEvent ChannelInfoEvent

// ChannelDeletedEvent represents the Channel deleted event
type ChannelDeletedEvent ChannelInfoEvent

// ChannelArchiveEvent represents the Channel archive event
type ChannelArchiveEvent ChannelInfoEvent

// ChannelUnarchiveEvent represents the Channel unarchive event
type ChannelUnarchiveEvent ChannelInfoEvent
golang-github-nlopes-slack-0.1.0/websocket_dm.go000066400000000000000000000013061311262777100216340ustar00rootroot00000000000000package slack

// IMCreatedEvent represents the IM created event
type IMCreatedEvent struct {
	Type    string             `json:"type"`
	User    string             `json:"user"`
	Channel ChannelCreatedInfo `json:"channel"`
}

// IMHistoryChangedEvent represents the IM history changed event
type IMHistoryChangedEvent ChannelHistoryChangedEvent

// IMOpenEvent represents the IM open event
type IMOpenEvent ChannelInfoEvent

// IMCloseEvent represents the IM close event
type IMCloseEvent ChannelInfoEvent

// IMMarkedEvent represents the IM marked event
type IMMarkedEvent ChannelInfoEvent

// IMMarkedHistoryChanged represents the IM marked history changed event
type IMMarkedHistoryChanged ChannelInfoEvent
golang-github-nlopes-slack-0.1.0/websocket_dnd.go000066400000000000000000000003271311262777100220030ustar00rootroot00000000000000package slack

// DNDUpdatedEvent represents the update event for Do Not Disturb
type DNDUpdatedEvent struct {
	Type   string    `json:"type"`
	User   string    `json:"user"`
	Status DNDStatus `json:"dnd_status"`
}
golang-github-nlopes-slack-0.1.0/websocket_files.go000066400000000000000000000026061311262777100223420ustar00rootroot00000000000000package slack

// FileActionEvent represents the File action event
type fileActionEvent struct {
	Type           string `json:"type"`
	EventTimestamp string `json:"event_ts"`
	File           File   `json:"file"`
	// FileID is used for FileDeletedEvent
	FileID string `json:"file_id,omitempty"`
}

// FileCreatedEvent represents the File created event
type FileCreatedEvent fileActionEvent

// FileSharedEvent represents the File shared event
type FileSharedEvent fileActionEvent

// FilePublicEvent represents the File public event
type FilePublicEvent fileActionEvent

// FileUnsharedEvent represents the File unshared event
type FileUnsharedEvent fileActionEvent

// FileChangeEvent represents the File change event
type FileChangeEvent fileActionEvent

// FileDeletedEvent represents the File deleted event
type FileDeletedEvent fileActionEvent

// FilePrivateEvent represents the File private event
type FilePrivateEvent fileActionEvent

// FileCommentAddedEvent represents the File comment added event
type FileCommentAddedEvent struct {
	fileActionEvent
	Comment Comment `json:"comment"`
}

// FileCommentEditedEvent represents the File comment edited event
type FileCommentEditedEvent struct {
	fileActionEvent
	Comment Comment `json:"comment"`
}

// FileCommentDeletedEvent represents the File comment deleted event
type FileCommentDeletedEvent struct {
	fileActionEvent
	Comment string `json:"comment"`
}
golang-github-nlopes-slack-0.1.0/websocket_groups.go000066400000000000000000000030231311262777100225510ustar00rootroot00000000000000package slack

// GroupCreatedEvent represents the Group created event
type GroupCreatedEvent struct {
	Type    string             `json:"type"`
	User    string             `json:"user"`
	Channel ChannelCreatedInfo `json:"channel"`
}

// XXX: Should we really do this? event.Group is probably nicer than event.Channel
// even though the api returns "channel"

// GroupMarkedEvent represents the Group marked event
type GroupMarkedEvent ChannelInfoEvent

// GroupOpenEvent represents the Group open event
type GroupOpenEvent ChannelInfoEvent

// GroupCloseEvent represents the Group close event
type GroupCloseEvent ChannelInfoEvent

// GroupArchiveEvent represents the Group archive event
type GroupArchiveEvent ChannelInfoEvent

// GroupUnarchiveEvent represents the Group unarchive event
type GroupUnarchiveEvent ChannelInfoEvent

// GroupLeftEvent represents the Group left event
type GroupLeftEvent ChannelInfoEvent

// GroupJoinedEvent represents the Group joined event
type GroupJoinedEvent ChannelJoinedEvent

// GroupRenameEvent represents the Group rename event
type GroupRenameEvent struct {
	Type      string          `json:"type"`
	Group     GroupRenameInfo `json:"channel"`
	Timestamp string          `json:"ts"`
}

// GroupRenameInfo represents the group info related to the renamed group
type GroupRenameInfo struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Created string `json:"created"`
}

// GroupHistoryChangedEvent represents the Group history changed event
type GroupHistoryChangedEvent ChannelHistoryChangedEvent
golang-github-nlopes-slack-0.1.0/websocket_internals.go000066400000000000000000000041151311262777100232340ustar00rootroot00000000000000package slack

import (
	"fmt"
	"time"
)

/**
 * Internal events, created by this lib and not mapped to Slack APIs.
 */

// ConnectedEvent is used for when we connect to Slack
type ConnectedEvent struct {
	ConnectionCount int // 1 = first time, 2 = second time
	Info            *Info
}

// ConnectionErrorEvent contains information about a connection error
type ConnectionErrorEvent struct {
	Attempt  int
	ErrorObj error
}

func (c *ConnectionErrorEvent) Error() string {
	return c.ErrorObj.Error()
}

// ConnectingEvent contains information about our connection attempt
type ConnectingEvent struct {
	Attempt         int // 1 = first attempt, 2 = second attempt
	ConnectionCount int
}

// DisconnectedEvent contains information about how we disconnected
type DisconnectedEvent struct {
	Intentional bool
}

// LatencyReport contains information about connection latency
type LatencyReport struct {
	Value time.Duration
}

// InvalidAuthEvent is used in case we can't even authenticate with the API
type InvalidAuthEvent struct{}

// UnmarshallingErrorEvent is used when there are issues deconstructing a response
type UnmarshallingErrorEvent struct {
	ErrorObj error
}

func (u UnmarshallingErrorEvent) Error() string {
	return u.ErrorObj.Error()
}

// MessageTooLongEvent is used when sending a message that is too long
type MessageTooLongEvent struct {
	Message   OutgoingMessage
	MaxLength int
}

func (m *MessageTooLongEvent) Error() string {
	return fmt.Sprintf("Message too long (max %d characters)", m.MaxLength)
}

// OutgoingErrorEvent contains information in case there were errors sending messages
type OutgoingErrorEvent struct {
	Message  OutgoingMessage
	ErrorObj error
}

func (o OutgoingErrorEvent) Error() string {
	return o.ErrorObj.Error()
}

// IncomingEventError contains information about an unexpected error receiving a websocket event
type IncomingEventError struct {
	ErrorObj error
}

func (i *IncomingEventError) Error() string {
	return i.ErrorObj.Error()
}

// AckErrorEvent i
type AckErrorEvent struct {
	ErrorObj error
}

func (a *AckErrorEvent) Error() string {
	return a.ErrorObj.Error()
}
golang-github-nlopes-slack-0.1.0/websocket_managed_conn.go000066400000000000000000000350161311262777100236520ustar00rootroot00000000000000package slack

import (
	"encoding/json"
	"fmt"
	"io"
	"reflect"
	"time"

	"golang.org/x/net/websocket"
)

// ManageConnection can be called on a Slack RTM instance returned by the
// NewRTM method. It will connect to the slack RTM API and handle all incoming
// and outgoing events. If a connection fails then it will attempt to reconnect
// and will notify any listeners through an error event on the IncomingEvents
// channel.
//
// If the connection ends and the disconnect was unintentional then this will
// attempt to reconnect.
//
// This should only be called once per slack API! Otherwise expect undefined
// behavior.
//
// The defined error events are located in websocket_internals.go.
func (rtm *RTM) ManageConnection() {
	var connectionCount int
	for {
		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 {
			return
		}
		rtm.info = info
		rtm.IncomingEvents <- RTMEvent{"connected", &ConnectedEvent{
			ConnectionCount: connectionCount,
			Info:            info,
		}}

		rtm.conn = conn
		rtm.isConnected = true

		keepRunning := make(chan bool)
		// we're now connected (or have failed fatally) so we can set up
		// listeners
		go rtm.handleIncomingEvents(keepRunning)

		// this should be a blocking call until the connection has ended
		rtm.handleEvents(keepRunning, 30*time.Second)

		// after being disconnected we need to check if it was intentional
		// if not then we should try to reconnect
		if rtm.wasIntentional {
			return
		}
		// else continue and run the loop again to connect
	}
}

// connect attempts to connect to the slack websocket API. It handles any
// errors that occur while connecting and will return once a connection
// has been successfully opened.
// 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) {
	// used to provide exponential backoff wait time with jitter before trying
	// to connect to slack again
	boff := &backoff{
		Min:    100 * time.Millisecond,
		Max:    5 * time.Minute,
		Factor: 2,
		Jitter: true,
	}

	for {
		// send connecting event
		rtm.IncomingEvents <- RTMEvent{"connecting", &ConnectingEvent{
			Attempt:         boff.attempts + 1,
			ConnectionCount: connectionCount,
		}}
		// attempt to start the connection
		info, conn, err := rtm.startRTMAndDial(useRTMStart)
		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") {
			rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}}
			return nil, nil, sErr
		}

		// any other errors are treated as recoverable and we try again after
		// sending the event along the IncomingEvents channel
		rtm.IncomingEvents <- RTMEvent{"connection_error", &ConnectionErrorEvent{
			Attempt:  boff.attempts,
			ErrorObj: err,
		}}
		// get time we should wait before attempting to connect again
		dur := boff.Duration()
		rtm.Debugf("reconnection %d failed: %s", boff.attempts+1, err)
		rtm.Debugln(" -> reconnecting in", dur)
		time.Sleep(dur)
	}
}

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

	if useRTMStart {
		info, url, err = rtm.StartRTM()
	} else {
		info, url, err = rtm.ConnectRTM()
	}
	if err != nil {
		return nil, nil, err
	}

	conn, err := websocketProxyDial(url, "http://api.slack.com")
	if err != nil {
		return nil, nil, err
	}
	return info, conn, err
}

// killConnection stops the websocket connection and signals to all goroutines
// that they should cease listening to the connection for events.
//
// This should not be called directly! Instead a boolean value (true for
// intentional, false otherwise) should be sent to the killChannel on the RTM.
func (rtm *RTM) killConnection(keepRunning chan bool, intentional bool) error {
	rtm.Debugln("killing connection")
	if rtm.isConnected {
		close(keepRunning)
	}
	rtm.isConnected = false
	rtm.wasIntentional = intentional
	err := rtm.conn.Close()
	rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{intentional}}
	return err
}

// handleEvents is a blocking function that handles all events. This sends
// pings when asked to (on rtm.forcePing) and upon every given elapsed
// 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)
	defer ticker.Stop()
	for {
		select {
		// catch "stop" signal on channel close
		case intentional := <-rtm.killChannel:
			_ = rtm.killConnection(keepRunning, intentional)
			return
			// send pings on ticker interval
		case <-ticker.C:
			err := rtm.ping()
			if err != nil {
				_ = rtm.killConnection(keepRunning, false)
				return
			}
		case <-rtm.forcePing:
			err := rtm.ping()
			if err != nil {
				_ = rtm.killConnection(keepRunning, false)
				return
			}
		// listen for messages that need to be sent
		case msg := <-rtm.outgoingMessages:
			rtm.sendOutgoingMessage(msg)
		// listen for incoming messages that need to be parsed
		case rawEvent := <-rtm.rawEvents:
			rtm.handleRawEvent(rawEvent)
		}
	}
}

// handleIncomingEvents monitors the RTM's opened websocket for any incoming
// events. It pushes the raw events onto the RTM channel rawEvents.
//
// This will stop executing once the RTM's keepRunning channel has been closed
// or has anything sent to it.
func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) {
	for {
		// non-blocking listen to see if channel is closed
		select {
		// catch "stop" signal on channel close
		case <-keepRunning:
			return
		default:
			rtm.receiveIncomingEvent()
		}
	}
}

func (rtm *RTM) sendWithDeadline(msg interface{}) error {
	// set a write deadline on the connection
	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 {
		return err
	}
	// remove write deadline
	return rtm.conn.SetWriteDeadline(time.Time{})
}

// sendOutgoingMessage sends the given OutgoingMessage to the slack websocket.
//
// It does not currently detect if a outgoing message fails due to a disconnect
// and instead lets a future failed 'PING' detect the failed connection.
func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) {
	rtm.Debugln("Sending message:", msg)
	if len(msg.Text) > MaxMessageTextLength {
		rtm.IncomingEvents <- RTMEvent{"outgoing_error", &MessageTooLongEvent{
			Message:   msg,
			MaxLength: MaxMessageTextLength,
		}}
		return
	}

	if err := rtm.sendWithDeadline(msg); err != nil {
		rtm.IncomingEvents <- RTMEvent{"outgoing_error", &OutgoingErrorEvent{
			Message:  msg,
			ErrorObj: err,
		}}
		// TODO force ping?
	}
}

// ping sends a 'PING' message to the RTM's websocket. If the 'PING' message
// fails to send then this returns an error signifying that the connection
// should be considered disconnected.
//
// This does not handle incoming 'PONG' responses but does store the time of
// each successful 'PING' send so latency can be detected upon a 'PONG'
// response.
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"}

	if err := rtm.sendWithDeadline(msg); err != nil {
		rtm.Debugf("RTM Error sending 'PING %d': %s", id, err.Error())
		return err
	}
	return nil
}

// 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() {
	event := json.RawMessage{}
	err := websocket.JSON.Receive(rtm.conn, &event)
	if err == io.EOF {
		// 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
		rtm.forcePing <- true
		return
	} else if err != nil {
		rtm.IncomingEvents <- RTMEvent{"incoming_error", &IncomingEventError{
			ErrorObj: err,
		}}
		// force a ping here too?
		return
	} else if len(event) == 0 {
		rtm.Debugln("Received empty event")
		return
	}
	rtm.Debugln("Incoming Event:", string(event[:]))
	rtm.rawEvents <- event
}

// handleRawEvent takes a raw JSON message received from the slack websocket
// and handles the encoded event.
func (rtm *RTM) handleRawEvent(rawEvent json.RawMessage) {
	event := &Event{}
	err := json.Unmarshal(rawEvent, event)
	if err != nil {
		rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}}
		return
	}
	switch event.Type {
	case "":
		rtm.handleAck(rawEvent)
	case "hello":
		rtm.IncomingEvents <- RTMEvent{"hello", &HelloEvent{}}
	case "pong":
		rtm.handlePong(rawEvent)
	default:
		rtm.handleEvent(event.Type, rawEvent)
	}
}

// handleAck handles an incoming 'ACK' message.
func (rtm *RTM) handleAck(event json.RawMessage) {
	ack := &AckMessage{}
	if err := json.Unmarshal(event, ack); err != nil {
		rtm.Debugln("RTM Error unmarshalling 'ack' event:", err)
		rtm.Debugln(" -> Erroneous 'ack' event:", string(event))
		return
	}
	if ack.Ok {
		rtm.IncomingEvents <- RTMEvent{"ack", ack}
	} else {
		rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error}}
	}
}

// handlePong handles an incoming 'PONG' message which should be in response to
// 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)
		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))
	}
}

// handleEvent is the "default" response to an event that does not have a
// special case. It matches the command's name to a mapping of defined events
// and then sends the corresponding event struct to the IncomingEvents channel.
// If the event type is not found or the event cannot be unmarshalled into the
// correct struct then this sends an UnmarshallingErrorEvent to the
// IncomingEvents channel.
func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) {
	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))
		rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}}
		return
	}
	t := reflect.TypeOf(v)
	recvEvent := reflect.New(t).Interface()
	err := json.Unmarshal(event, recvEvent)
	if err != nil {
		rtm.Debugf("RTM Error, could not unmarshall event %q: %s\n", typeStr, string(event))
		err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s\n", typeStr, string(event))
		rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}}
		return
	}
	rtm.IncomingEvents <- RTMEvent{typeStr, recvEvent}
}

// 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{}{
	"message":         MessageEvent{},
	"presence_change": PresenceChangeEvent{},
	"user_typing":     UserTypingEvent{},

	"channel_marked":          ChannelMarkedEvent{},
	"channel_created":         ChannelCreatedEvent{},
	"channel_joined":          ChannelJoinedEvent{},
	"channel_left":            ChannelLeftEvent{},
	"channel_deleted":         ChannelDeletedEvent{},
	"channel_rename":          ChannelRenameEvent{},
	"channel_archive":         ChannelArchiveEvent{},
	"channel_unarchive":       ChannelUnarchiveEvent{},
	"channel_history_changed": ChannelHistoryChangedEvent{},

	"dnd_updated":      DNDUpdatedEvent{},
	"dnd_updated_user": DNDUpdatedEvent{},

	"im_created":         IMCreatedEvent{},
	"im_open":            IMOpenEvent{},
	"im_close":           IMCloseEvent{},
	"im_marked":          IMMarkedEvent{},
	"im_history_changed": IMHistoryChangedEvent{},

	"group_marked":          GroupMarkedEvent{},
	"group_open":            GroupOpenEvent{},
	"group_joined":          GroupJoinedEvent{},
	"group_left":            GroupLeftEvent{},
	"group_close":           GroupCloseEvent{},
	"group_rename":          GroupRenameEvent{},
	"group_archive":         GroupArchiveEvent{},
	"group_unarchive":       GroupUnarchiveEvent{},
	"group_history_changed": GroupHistoryChangedEvent{},

	"file_created":         FileCreatedEvent{},
	"file_shared":          FileSharedEvent{},
	"file_unshared":        FileUnsharedEvent{},
	"file_public":          FilePublicEvent{},
	"file_private":         FilePrivateEvent{},
	"file_change":          FileChangeEvent{},
	"file_deleted":         FileDeletedEvent{},
	"file_comment_added":   FileCommentAddedEvent{},
	"file_comment_edited":  FileCommentEditedEvent{},
	"file_comment_deleted": FileCommentDeletedEvent{},

	"pin_added":   PinAddedEvent{},
	"pin_removed": PinRemovedEvent{},

	"star_added":   StarAddedEvent{},
	"star_removed": StarRemovedEvent{},

	"reaction_added":   ReactionAddedEvent{},
	"reaction_removed": ReactionRemovedEvent{},

	"pref_change": PrefChangeEvent{},

	"team_join":              TeamJoinEvent{},
	"team_rename":            TeamRenameEvent{},
	"team_pref_change":       TeamPrefChangeEvent{},
	"team_domain_change":     TeamDomainChangeEvent{},
	"team_migration_started": TeamMigrationStartedEvent{},

	"manual_presence_change": ManualPresenceChangeEvent{},

	"user_change": UserChangeEvent{},

	"emoji_changed": EmojiChangedEvent{},

	"commands_changed": CommandsChangedEvent{},

	"email_domain_changed": EmailDomainChangedEvent{},

	"bot_added":   BotAddedEvent{},
	"bot_changed": BotChangedEvent{},

	"accounts_changed": AccountsChangedEvent{},

	"reconnect_url": ReconnectUrlEvent{},
}
golang-github-nlopes-slack-0.1.0/websocket_misc.go000066400000000000000000000061211311262777100221670ustar00rootroot00000000000000package slack

import (
	"encoding/json"
	"fmt"
)

// AckMessage is used for messages received in reply to other messages
type AckMessage struct {
	ReplyTo   int    `json:"reply_to"`
	Timestamp string `json:"ts"`
	Text      string `json:"text"`
	RTMResponse
}

// RTMResponse encapsulates response details as returned by the Slack API
type RTMResponse struct {
	Ok    bool      `json:"ok"`
	Error *RTMError `json:"error"`
}

// RTMError encapsulates error information as returned by the Slack API
type RTMError struct {
	Code int
	Msg  string
}

func (s RTMError) Error() string {
	return fmt.Sprintf("Code %d - %s", s.Code, s.Msg)
}

// MessageEvent represents a Slack Message (used as the event type for an incoming message)
type MessageEvent Message

// RTMEvent is the main wrapper. You will find all the other messages attached
type RTMEvent struct {
	Type string
	Data interface{}
}

// HelloEvent represents the hello event
type HelloEvent struct{}

// PresenceChangeEvent represents the presence change event
type PresenceChangeEvent struct {
	Type     string `json:"type"`
	Presence string `json:"presence"`
	User     string `json:"user"`
}

// UserTypingEvent represents the user typing event
type UserTypingEvent struct {
	Type    string `json:"type"`
	User    string `json:"user"`
	Channel string `json:"channel"`
}

// PrefChangeEvent represents a user preferences change event
type PrefChangeEvent struct {
	Type  string          `json:"type"`
	Name  string          `json:"name"`
	Value json.RawMessage `json:"value"`
}

// ManualPresenceChangeEvent represents the manual presence change event
type ManualPresenceChangeEvent struct {
	Type     string `json:"type"`
	Presence string `json:"presence"`
}

// UserChangeEvent represents the user change event
type UserChangeEvent struct {
	Type string `json:"type"`
	User User   `json:"user"`
}

// EmojiChangedEvent represents the emoji changed event
type EmojiChangedEvent struct {
	Type           string   `json:"type"`
	SubType        string   `json:"subtype"`
	Name           string   `json:"name"`
	Names          []string `json:"names"`
	Value          string   `json:"value"` 
	EventTimestamp string   `json:"event_ts"`
}

// CommandsChangedEvent represents the commands changed event
type CommandsChangedEvent struct {
	Type           string `json:"type"`
	EventTimestamp string `json:"event_ts"`
}

// EmailDomainChangedEvent represents the email domain changed event
type EmailDomainChangedEvent struct {
	Type           string `json:"type"`
	EventTimestamp string `json:"event_ts"`
	EmailDomain    string `json:"email_domain"`
}

// BotAddedEvent represents the bot added event
type BotAddedEvent struct {
	Type string `json:"type"`
	Bot  Bot    `json:"bot"`
}

// BotChangedEvent represents the bot changed event
type BotChangedEvent struct {
	Type string `json:"type"`
	Bot  Bot    `json:"bot"`
}

// AccountsChangedEvent represents the accounts changed event
type AccountsChangedEvent struct {
	Type string `json:"type"`
}

// ReconnectUrlEvent represents the receiving reconnect url event
type ReconnectUrlEvent struct {
	Type string `json:"type"`
	URL  string `json:"url"`
}
golang-github-nlopes-slack-0.1.0/websocket_pins.go000066400000000000000000000006761311262777100222160ustar00rootroot00000000000000package slack

type pinEvent struct {
	Type           string `json:"type"`
	User           string `json:"user"`
	Item           Item   `json:"item"`
	Channel        string `json:"channel_id"`
	EventTimestamp string `json:"event_ts"`
	HasPins        bool   `json:"has_pins,omitempty"`
}

// PinAddedEvent represents the Pin added event
type PinAddedEvent pinEvent

// PinRemovedEvent represents the Pin removed event
type PinRemovedEvent pinEvent
golang-github-nlopes-slack-0.1.0/websocket_proxy.go000066400000000000000000000030111311262777100224100ustar00rootroot00000000000000package 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)
	cc.Do(&req)
	if err != nil && err != httputil.ErrPersistEOF {
		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)
}
golang-github-nlopes-slack-0.1.0/websocket_reactions.go000066400000000000000000000015161311262777100232260ustar00rootroot00000000000000package slack

// reactionItem is a lighter-weight item than is returned by the reactions list.
type reactionItem struct {
	Type        string `json:"type"`
	Channel     string `json:"channel,omitempty"`
	File        string `json:"file,omitempty"`
	FileComment string `json:"file_comment,omitempty"`
	Timestamp   string `json:"ts,omitempty"`
}

type reactionEvent struct {
	Type           string       `json:"type"`
	User           string       `json:"user"`
	ItemUser       string       `json:"item_user"`
	Item           reactionItem `json:"item"`
	Reaction       string       `json:"reaction"`
	EventTimestamp string       `json:"event_ts"`
}

// ReactionAddedEvent represents the Reaction added event
type ReactionAddedEvent reactionEvent

// ReactionRemovedEvent represents the Reaction removed event
type ReactionRemovedEvent reactionEvent
golang-github-nlopes-slack-0.1.0/websocket_stars.go000066400000000000000000000005751311262777100223770ustar00rootroot00000000000000package slack

type starEvent struct {
	Type           string      `json:"type"`
	User           string      `json:"user"`
	Item           StarredItem `json:"item"`
	EventTimestamp string      `json:"event_ts"`
}

// StarAddedEvent represents the Star added event
type StarAddedEvent starEvent

// StarRemovedEvent represents the Star removed event
type StarRemovedEvent starEvent
golang-github-nlopes-slack-0.1.0/websocket_teams.go000066400000000000000000000016251311262777100223510ustar00rootroot00000000000000package slack

// TeamJoinEvent represents the Team join event
type TeamJoinEvent struct {
	Type string `json:"type"`
	User User   `json:"user"`
}

// TeamRenameEvent represents the Team rename event
type TeamRenameEvent struct {
	Type           string `json:"type"`
	Name           string `json:"name,omitempty"`
	EventTimestamp string `json:"event_ts,omitempty"`
}

// TeamPrefChangeEvent represents the Team preference change event
type TeamPrefChangeEvent struct {
	Type  string   `json:"type"`
	Name  string   `json:"name,omitempty"`
	Value []string `json:"value,omitempty"`
}

// TeamDomainChangeEvent represents the Team domain change event
type TeamDomainChangeEvent struct {
	Type   string `json:"type"`
	URL    string `json:"url"`
	Domain string `json:"domain"`
}

// TeamMigrationStartedEvent represents the Team migration started event
type TeamMigrationStartedEvent struct {
	Type string `json:"type"`
}
golang-github-nlopes-slack-0.1.0/websocket_utils.go000066400000000000000000000006521311262777100223770ustar00rootroot00000000000000package slack

import (
	"net"
	"net/url"
)

var portMapping = map[string]string{"ws": "80", "wss": "443"}

func websocketizeURLPort(orig string) (string, error) {
	urlObj, err := url.ParseRequestURI(orig)
	if err != nil {
		return "", err
	}
	_, _, err = net.SplitHostPort(urlObj.Host)
	if err != nil {
		return urlObj.Scheme + "://" + urlObj.Host + ":" + portMapping[urlObj.Scheme] + urlObj.Path, nil
	}
	return orig, nil
}