summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWim <wim@42.be>2016-09-05 16:34:37 +0200
committerWim <wim@42.be>2016-09-05 16:34:37 +0200
commitb30e85836e575974bac1b2ea91e9b6c2dd39fe44 (patch)
treeb3aa5d148e436cacd6c737ad56ba3fc0b9369e26
parente449a97bd0114e55c2a73aa79b061b55d755aa16 (diff)
downloadmatterbridge-msglm-b30e85836e575974bac1b2ea91e9b6c2dd39fe44.tar.gz
matterbridge-msglm-b30e85836e575974bac1b2ea91e9b6c2dd39fe44.tar.bz2
matterbridge-msglm-b30e85836e575974bac1b2ea91e9b6c2dd39fe44.zip
Add Slack support
-rw-r--r--bridge/bridge.go9
-rw-r--r--bridge/config/config.go15
-rw-r--r--bridge/slack/slack.go180
-rw-r--r--matterhook/matterhook.go4
-rw-r--r--vendor/github.com/nlopes/slack/LICENSE23
-rw-r--r--vendor/github.com/nlopes/slack/admin.go190
-rw-r--r--vendor/github.com/nlopes/slack/attachments.go76
-rw-r--r--vendor/github.com/nlopes/slack/backoff.go57
-rw-r--r--vendor/github.com/nlopes/slack/channels.go261
-rw-r--r--vendor/github.com/nlopes/slack/chat.go166
-rw-r--r--vendor/github.com/nlopes/slack/comment.go10
-rw-r--r--vendor/github.com/nlopes/slack/conversation.go38
-rw-r--r--vendor/github.com/nlopes/slack/dnd.go123
-rw-r--r--vendor/github.com/nlopes/slack/emoji.go27
-rw-r--r--vendor/github.com/nlopes/slack/examples/channels/channels.go19
-rw-r--r--vendor/github.com/nlopes/slack/examples/files/files.go30
-rw-r--r--vendor/github.com/nlopes/slack/examples/groups/groups.go22
-rw-r--r--vendor/github.com/nlopes/slack/examples/messages/messages.go32
-rw-r--r--vendor/github.com/nlopes/slack/examples/pins/pins.go123
-rw-r--r--vendor/github.com/nlopes/slack/examples/reactions/reactions.go126
-rw-r--r--vendor/github.com/nlopes/slack/examples/stars/stars.go46
-rw-r--r--vendor/github.com/nlopes/slack/examples/users/users.go17
-rw-r--r--vendor/github.com/nlopes/slack/examples/websocket/websocket.go58
-rw-r--r--vendor/github.com/nlopes/slack/files.go274
-rw-r--r--vendor/github.com/nlopes/slack/groups.go293
-rw-r--r--vendor/github.com/nlopes/slack/history.go36
-rw-r--r--vendor/github.com/nlopes/slack/im.go130
-rw-r--r--vendor/github.com/nlopes/slack/info.go206
-rw-r--r--vendor/github.com/nlopes/slack/item.go75
-rw-r--r--vendor/github.com/nlopes/slack/messageID.go30
-rw-r--r--vendor/github.com/nlopes/slack/messages.go131
-rw-r--r--vendor/github.com/nlopes/slack/misc.go119
-rw-r--r--vendor/github.com/nlopes/slack/oauth.go54
-rw-r--r--vendor/github.com/nlopes/slack/pagination.go20
-rw-r--r--vendor/github.com/nlopes/slack/pins.go79
-rw-r--r--vendor/github.com/nlopes/slack/reactions.go246
-rw-r--r--vendor/github.com/nlopes/slack/rtm.go39
-rw-r--r--vendor/github.com/nlopes/slack/search.go137
-rw-r--r--vendor/github.com/nlopes/slack/slack.go88
-rw-r--r--vendor/github.com/nlopes/slack/stars.go135
-rw-r--r--vendor/github.com/nlopes/slack/team.go46
-rw-r--r--vendor/github.com/nlopes/slack/users.go140
-rw-r--r--vendor/github.com/nlopes/slack/websocket.go93
-rw-r--r--vendor/github.com/nlopes/slack/websocket_channels.go72
-rw-r--r--vendor/github.com/nlopes/slack/websocket_dm.go23
-rw-r--r--vendor/github.com/nlopes/slack/websocket_dnd.go8
-rw-r--r--vendor/github.com/nlopes/slack/websocket_files.go49
-rw-r--r--vendor/github.com/nlopes/slack/websocket_groups.go49
-rw-r--r--vendor/github.com/nlopes/slack/websocket_internals.go92
-rw-r--r--vendor/github.com/nlopes/slack/websocket_managed_conn.go427
-rw-r--r--vendor/github.com/nlopes/slack/websocket_misc.go117
-rw-r--r--vendor/github.com/nlopes/slack/websocket_pins.go16
-rw-r--r--vendor/github.com/nlopes/slack/websocket_proxy.go83
-rw-r--r--vendor/github.com/nlopes/slack/websocket_reactions.go25
-rw-r--r--vendor/github.com/nlopes/slack/websocket_stars.go14
-rw-r--r--vendor/github.com/nlopes/slack/websocket_teams.go33
-rw-r--r--vendor/github.com/nlopes/slack/websocket_utils.go20
-rw-r--r--vendor/golang.org/x/net/websocket/LICENSE27
-rw-r--r--vendor/golang.org/x/net/websocket/client.go113
-rw-r--r--vendor/golang.org/x/net/websocket/hybi.go583
-rw-r--r--vendor/golang.org/x/net/websocket/server.go113
-rw-r--r--vendor/golang.org/x/net/websocket/websocket.go411
-rw-r--r--vendor/manifest17
63 files changed, 6314 insertions, 1 deletions
diff --git a/bridge/bridge.go b/bridge/bridge.go
index 047370be..45a0160a 100644
--- a/bridge/bridge.go
+++ b/bridge/bridge.go
@@ -6,6 +6,7 @@ import (
"github.com/42wim/matterbridge/bridge/gitter"
"github.com/42wim/matterbridge/bridge/irc"
"github.com/42wim/matterbridge/bridge/mattermost"
+ "github.com/42wim/matterbridge/bridge/slack"
"github.com/42wim/matterbridge/bridge/xmpp"
log "github.com/Sirupsen/logrus"
"strings"
@@ -42,6 +43,9 @@ func NewBridge(cfg *config.Config) error {
if cfg.Gitter.Enable {
b.Bridges = append(b.Bridges, bgitter.New(cfg, c))
}
+ if cfg.Slack.Enable {
+ b.Bridges = append(b.Bridges, bslack.New(cfg, c))
+ }
if len(b.Bridges) < 2 {
log.Fatalf("only %d sections enabled. Need at least 2 sections enabled (eg [IRC] and [mattermost]", len(b.Bridges))
}
@@ -72,6 +76,7 @@ func (b *Bridge) mapChannels() error {
m["mattermost"] = val.Mattermost
m["xmpp"] = val.Xmpp
m["gitter"] = val.Gitter
+ m["slack"] = val.Slack
b.Channels = append(b.Channels, m)
}
return nil
@@ -83,6 +88,7 @@ func (b *Bridge) mapIgnores() {
m["mattermost"] = strings.Fields(b.Config.Mattermost.IgnoreNicks)
m["xmpp"] = strings.Fields(b.Config.Xmpp.IgnoreNicks)
m["gitter"] = strings.Fields(b.Config.Gitter.IgnoreNicks)
+ m["slack"] = strings.Fields(b.Config.Slack.IgnoreNicks)
b.ignoreNicks = m
}
@@ -105,6 +111,7 @@ func (b *Bridge) handleMessage(msg config.Message, dest Bridger) {
return
}
b.modifyMessage(&msg, dest.Name())
+ log.Debugf("sending %#v from %s to %s", msg, msg.Origin, dest.Name())
dest.Send(msg)
}
}
@@ -138,5 +145,7 @@ func (b *Bridge) modifyMessage(msg *config.Message, dest string) {
setNickFormat(msg, b.Config.Xmpp.RemoteNickFormat)
case "mattermost":
setNickFormat(msg, b.Config.Mattermost.RemoteNickFormat)
+ case "slack":
+ setNickFormat(msg, b.Config.Slack.RemoteNickFormat)
}
}
diff --git a/bridge/config/config.go b/bridge/config/config.go
index cd353fcb..48574e28 100644
--- a/bridge/config/config.go
+++ b/bridge/config/config.go
@@ -35,7 +35,6 @@ type Config struct {
RemoteNickFormat string
Token string
}
-
Mattermost struct {
URL string
ShowJoinPart bool
@@ -55,6 +54,19 @@ type Config struct {
NoTLS bool
Enable bool
}
+ Slack struct {
+ BindAddress string
+ Enable bool
+ IconURL string
+ IgnoreNicks string
+ NickFormatter string
+ NicksPerRow int
+ PrefixMessagesWithNick bool
+ RemoteNickFormat string
+ Token string
+ URL string
+ UseAPI bool
+ }
Xmpp struct {
IgnoreNicks string
Jid string
@@ -70,6 +82,7 @@ type Config struct {
Mattermost string
Xmpp string
Gitter string
+ Slack string
}
General struct {
GiphyAPIKey string
diff --git a/bridge/slack/slack.go b/bridge/slack/slack.go
new file mode 100644
index 00000000..804eaea4
--- /dev/null
+++ b/bridge/slack/slack.go
@@ -0,0 +1,180 @@
+package bslack
+
+import (
+ "github.com/42wim/matterbridge/bridge/config"
+ "github.com/42wim/matterbridge/matterhook"
+ log "github.com/Sirupsen/logrus"
+ "github.com/nlopes/slack"
+ "strings"
+ "time"
+)
+
+type MMMessage struct {
+ Text string
+ Channel string
+ Username string
+}
+
+type bslack struct {
+ mh *matterhook.Client
+ sc *slack.Client
+ // MMapi
+ *config.Config
+ rtm *slack.RTM
+ Plus bool
+ Remote chan config.Message
+ channels []slack.Channel
+}
+
+var flog *log.Entry
+
+func init() {
+ flog = log.WithFields(log.Fields{"module": "slack"})
+}
+
+func New(cfg *config.Config, c chan config.Message) *bslack {
+ b := &bslack{}
+ b.Config = cfg
+ b.Remote = c
+ b.Plus = cfg.Slack.UseAPI
+ return b
+}
+
+func (b *bslack) Command(cmd string) string {
+ return ""
+}
+
+func (b *bslack) Connect() error {
+ if !b.Plus {
+ b.mh = matterhook.New(b.Config.Slack.URL,
+ matterhook.Config{BindAddress: b.Config.Slack.BindAddress})
+ } else {
+ b.sc = slack.New(b.Config.Slack.Token)
+ flog.Infof("Trying login on slack with Token")
+ /*
+ if err != nil {
+ return err
+ }
+ */
+ flog.Info("Login ok")
+ }
+ b.rtm = b.sc.NewRTM()
+ go b.rtm.ManageConnection()
+ go b.handleSlack()
+ return nil
+}
+
+func (b *bslack) Name() string {
+ return "slack"
+}
+
+func (b *bslack) Send(msg config.Message) error {
+ flog.Infof("slack send %#v", msg)
+ if msg.Origin != "slack" {
+ return b.SendType(msg.Username, msg.Text, msg.Channel, "")
+ }
+ return nil
+}
+
+func (b *bslack) SendType(nick string, message string, channel string, mtype string) error {
+ if b.Config.Slack.PrefixMessagesWithNick {
+ message = nick + " " + message
+ }
+ if !b.Plus {
+ matterMessage := matterhook.OMessage{IconURL: b.Config.Slack.IconURL}
+ matterMessage.Channel = channel
+ matterMessage.UserName = nick
+ matterMessage.Type = mtype
+ matterMessage.Text = message
+ err := b.mh.Send(matterMessage)
+ if err != nil {
+ flog.Info(err)
+ return err
+ }
+ flog.Debug("->slack channel: ", channel, " ", message)
+ return nil
+ }
+ flog.Debugf("sent to slack channel API: %s %s", channel, message)
+ newmsg := b.rtm.NewOutgoingMessage(message, b.getChannelByName(channel).ID)
+ b.rtm.SendMessage(newmsg)
+ return nil
+}
+
+func (b *bslack) getChannelByName(name string) *slack.Channel {
+ if b.channels == nil {
+ return nil
+ }
+ for _, channel := range b.channels {
+ if channel.Name == name {
+ return &channel
+ }
+ }
+ return nil
+}
+
+func (b *bslack) handleSlack() {
+ flog.Infof("Choosing API based slack connection: %t", b.Plus)
+ mchan := make(chan *MMMessage)
+ if b.Plus {
+ go b.handleSlackClient(mchan)
+ } else {
+ go b.handleMatterHook(mchan)
+ }
+ time.Sleep(time.Second)
+ flog.Info("Start listening for Slack messages")
+ for message := range mchan {
+ texts := strings.Split(message.Text, "\n")
+ for _, text := range texts {
+ flog.Debug("Sending message from " + message.Username + " to " + message.Channel)
+ b.Remote <- config.Message{Text: text, Username: message.Username, Channel: message.Channel, Origin: "slack"}
+ }
+ }
+}
+
+func (b *bslack) handleSlackClient(mchan chan *MMMessage) {
+ for msg := range b.rtm.IncomingEvents {
+ switch ev := msg.Data.(type) {
+ case *slack.MessageEvent:
+ flog.Debugf("%#v", ev)
+ channel, err := b.rtm.GetChannelInfo(ev.Channel)
+ if err != nil {
+ continue
+ }
+ user, err := b.rtm.GetUserInfo(ev.User)
+ if err != nil {
+ continue
+ }
+ m := &MMMessage{}
+ m.Username = user.Name
+ m.Channel = channel.Name
+ m.Text = ev.Text
+ mchan <- m
+ case *slack.OutgoingErrorEvent:
+ flog.Debugf("%#v", ev.Error())
+ case *slack.ConnectedEvent:
+ b.channels = ev.Info.Channels
+ for _, val := range b.Config.Channel {
+ channel := b.getChannelByName(val.Slack)
+ if channel != nil && !channel.IsMember {
+ flog.Infof("Joining %s", val.Slack)
+ b.sc.JoinChannel(channel.ID)
+ }
+ }
+ case *slack.InvalidAuthEvent:
+ flog.Fatalf("Invalid Token %#v", ev)
+ default:
+ }
+ }
+}
+
+func (b *bslack) handleMatterHook(mchan chan *MMMessage) {
+ for {
+ message := b.mh.Receive()
+ flog.Debugf("receiving from slack %#v", message)
+ m := &MMMessage{}
+ m.Username = message.UserName
+ m.Text = message.Text
+ m.Channel = message.ChannelName
+ mchan <- m
+ }
+}
diff --git a/matterhook/matterhook.go b/matterhook/matterhook.go
index fc003cce..0880884b 100644
--- a/matterhook/matterhook.go
+++ b/matterhook/matterhook.go
@@ -27,6 +27,8 @@ type OMessage struct {
// IMessage for mattermost outgoing webhook. (received from mattermost)
type IMessage struct {
+ BotID string `schema:"bot_id"`
+ BotName string `schema:"bot_name"`
Token string `schema:"token"`
TeamID string `schema:"team_id"`
TeamDomain string `schema:"team_domain"`
@@ -36,6 +38,8 @@ type IMessage struct {
UserID string `schema:"user_id"`
UserName string `schema:"user_name"`
PostId string `schema:"post_id"`
+ RawText string `schema:"raw_text"`
+ ServiceId string `schema:"service_id"`
Text string `schema:"text"`
TriggerWord string `schema:"trigger_word"`
}
diff --git a/vendor/github.com/nlopes/slack/LICENSE b/vendor/github.com/nlopes/slack/LICENSE
new file mode 100644
index 00000000..5145171f
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/LICENSE
@@ -0,0 +1,23 @@
+Copyright (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.
diff --git a/vendor/github.com/nlopes/slack/admin.go b/vendor/github.com/nlopes/slack/admin.go
new file mode 100644
index 00000000..393b2880
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/admin.go
@@ -0,0 +1,190 @@
+package 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
+}
diff --git a/vendor/github.com/nlopes/slack/attachments.go b/vendor/github.com/nlopes/slack/attachments.go
new file mode 100644
index 00000000..4faf3e3f
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/attachments.go
@@ -0,0 +1,76 @@
+package slack
+
+// 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 to be included in the attachment. Required when
+// using message buttons 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"
+ Value string `json:"value,omitempty"` // Optional.
+ Confirm []ConfirmationField `json:"confirm,omitempty"` // Optional.
+}
+
+// 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 int64 `json:"ts,omitempty"`
+}
diff --git a/vendor/github.com/nlopes/slack/backoff.go b/vendor/github.com/nlopes/slack/backoff.go
new file mode 100644
index 00000000..e555a1ad
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/backoff.go
@@ -0,0 +1,57 @@
+package 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
+}
diff --git a/vendor/github.com/nlopes/slack/channels.go b/vendor/github.com/nlopes/slack/channels.go
new file mode 100644
index 00000000..05cfbe1d
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/channels.go
@@ -0,0 +1,261 @@
+package 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
+}
diff --git a/vendor/github.com/nlopes/slack/chat.go b/vendor/github.com/nlopes/slack/chat.go
new file mode 100644
index 00000000..52a3420f
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/chat.go
@@ -0,0 +1,166 @@
+package slack
+
+import (
+ "encoding/json"
+ "errors"
+ "net/url"
+ "strings"
+)
+
+const (
+ DEFAULT_MESSAGE_USERNAME = ""
+ 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
+ Username string
+ AsUser bool
+ Parse string
+ LinkNames int
+ Attachments []Attachment
+ UnfurlLinks bool
+ UnfurlMedia bool
+ IconURL string
+ IconEmoji string
+ Markdown bool `json:"mrkdwn,omitempty"`
+ EscapeText bool
+}
+
+// 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,
+ }
+}
+
+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
+}
+
+// DeleteMessage deletes a message in a channel
+func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) {
+ values := url.Values{
+ "token": {api.config.token},
+ "channel": {channel},
+ "ts": {messageTimestamp},
+ }
+ response, err := chatRequest("chat.delete", values, api.debug)
+ if err != nil {
+ return "", "", err
+ }
+ return response.Channel, response.Timestamp, nil
+}
+
+func escapeMessage(message string) string {
+ replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
+ return replacer.Replace(message)
+}
+
+// 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) {
+ if params.EscapeText {
+ text = escapeMessage(text)
+ }
+ values := url.Values{
+ "token": {api.config.token},
+ "channel": {channel},
+ "text": {text},
+ }
+ if params.Username != DEFAULT_MESSAGE_USERNAME {
+ values.Set("username", string(params.Username))
+ }
+ if params.AsUser != DEFAULT_MESSAGE_ASUSER {
+ values.Set("as_user", "true")
+ }
+ if params.Parse != DEFAULT_MESSAGE_PARSE {
+ values.Set("parse", string(params.Parse))
+ }
+ if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES {
+ values.Set("link_names", "1")
+ }
+ if params.Attachments != nil {
+ attachments, err := json.Marshal(params.Attachments)
+ if err != nil {
+ return "", "", err
+ }
+ values.Set("attachments", string(attachments))
+ }
+ if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS {
+ 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 {
+ values.Set("unfurl_links", "false")
+ }
+ if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA {
+ values.Set("unfurl_media", "false")
+ }
+ if params.IconURL != DEFAULT_MESSAGE_ICON_URL {
+ values.Set("icon_url", params.IconURL)
+ }
+ if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI {
+ values.Set("icon_emoji", params.IconEmoji)
+ }
+ if params.Markdown != DEFAULT_MESSAGE_MARKDOWN {
+ values.Set("mrkdwn", "false")
+ }
+
+ response, err := chatRequest("chat.postMessage", values, api.debug)
+ if err != nil {
+ return "", "", err
+ }
+ return response.Channel, response.Timestamp, nil
+}
+
+// UpdateMessage updates a message in a channel
+func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) {
+ values := url.Values{
+ "token": {api.config.token},
+ "channel": {channel},
+ "text": {escapeMessage(text)},
+ "ts": {timestamp},
+ }
+ response, err := chatRequest("chat.update", values, api.debug)
+ if err != nil {
+ return "", "", "", err
+ }
+ return response.Channel, response.Timestamp, response.Text, nil
+}
diff --git a/vendor/github.com/nlopes/slack/comment.go b/vendor/github.com/nlopes/slack/comment.go
new file mode 100644
index 00000000..7d1c0d4e
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/comment.go
@@ -0,0 +1,10 @@
+package 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"`
+}
diff --git a/vendor/github.com/nlopes/slack/conversation.go b/vendor/github.com/nlopes/slack/conversation.go
new file mode 100644
index 00000000..51b993d1
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/conversation.go
@@ -0,0 +1,38 @@
+package 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"`
+ NumMembers int `json:"num_members,omitempty"`
+ 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"`
+}
diff --git a/vendor/github.com/nlopes/slack/dnd.go b/vendor/github.com/nlopes/slack/dnd.go
new file mode 100644
index 00000000..ac87758d
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/dnd.go
@@ -0,0 +1,123 @@
+package 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
+}
diff --git a/vendor/github.com/nlopes/slack/emoji.go b/vendor/github.com/nlopes/slack/emoji.go
new file mode 100644
index 00000000..776c4a5f
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/emoji.go
@@ -0,0 +1,27 @@
+package 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
+}
diff --git a/vendor/github.com/nlopes/slack/examples/channels/channels.go b/vendor/github.com/nlopes/slack/examples/channels/channels.go
new file mode 100644
index 00000000..a4465bdb
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/examples/channels/channels.go
@@ -0,0 +1,19 @@
+package 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)
+ }
+}
diff --git a/vendor/github.com/nlopes/slack/examples/files/files.go b/vendor/github.com/nlopes/slack/examples/files/files.go
new file mode 100644
index 00000000..bb7b4e50
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/examples/files/files.go
@@ -0,0 +1,30 @@
+package 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)
+}
diff --git a/vendor/github.com/nlopes/slack/examples/groups/groups.go b/vendor/github.com/nlopes/slack/examples/groups/groups.go
new file mode 100644
index 00000000..2af215d1
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/examples/groups/groups.go
@@ -0,0 +1,22 @@
+package 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)
+ }
+}
diff --git a/vendor/github.com/nlopes/slack/examples/messages/messages.go b/vendor/github.com/nlopes/slack/examples/messages/messages.go
new file mode 100644
index 00000000..b3ea87f3
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/examples/messages/messages.go
@@ -0,0 +1,32 @@
+package 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)
+}
diff --git a/vendor/github.com/nlopes/slack/examples/pins/pins.go b/vendor/github.com/nlopes/slack/examples/pins/pins.go
new file mode 100644
index 00000000..d225184c
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/examples/pins/pins.go
@@ -0,0 +1,123 @@
+package 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
+ }
+
+}
diff --git a/vendor/github.com/nlopes/slack/examples/reactions/reactions.go b/vendor/github.com/nlopes/slack/examples/reactions/reactions.go
new file mode 100644
index 00000000..753f0d25
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/examples/reactions/reactions.go
@@ -0,0 +1,126 @@
+package 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)
+ }
+}
diff --git a/vendor/github.com/nlopes/slack/examples/stars/stars.go b/vendor/github.com/nlopes/slack/examples/stars/stars.go
new file mode 100644
index 00000000..d20c3dcf
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/examples/stars/stars.go
@@ -0,0 +1,46 @@
+package 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)
+ }
+}
diff --git a/vendor/github.com/nlopes/slack/examples/users/users.go b/vendor/github.com/nlopes/slack/examples/users/users.go
new file mode 100644
index 00000000..9a6e1f6f
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/examples/users/users.go
@@ -0,0 +1,17 @@
+package 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)
+}
diff --git a/vendor/github.com/nlopes/slack/examples/websocket/websocket.go b/vendor/github.com/nlopes/slack/examples/websocket/websocket.go
new file mode 100644
index 00000000..612b97c0
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/examples/websocket/websocket.go
@@ -0,0 +1,58 @@
+package 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()
+
+Loop:
+ for {
+ select {
+ case msg := <-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")
+ break Loop
+
+ default:
+
+ // Ignore other events..
+ // fmt.Printf("Unexpected: %v\n", msg.Data)
+ }
+ }
+ }
+}
diff --git a/vendor/github.com/nlopes/slack/files.go b/vendor/github.com/nlopes/slack/files.go
new file mode 100644
index 00000000..18fc9ba9
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/files.go
@@ -0,0 +1,274 @@
+package slack
+
+import (
+ "errors"
+ "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
+type FileUploadParameters struct {
+ File string
+ Content string
+ 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)
+ }
+ // XXX: this is broken. fix it with a proper unix timestamp
+ if params.TimestampFrom != DEFAULT_FILES_TS_FROM {
+ values.Add("ts_from", params.TimestampFrom.String())
+ }
+ if params.TimestampTo != DEFAULT_FILES_TS_TO {
+ values.Add("ts_to", params.TimestampTo.String())
+ }
+ 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 = postWithMultipartResponse("files.upload", params.File, values, 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
+}
diff --git a/vendor/github.com/nlopes/slack/groups.go b/vendor/github.com/nlopes/slack/groups.go
new file mode 100644
index 00000000..ec4a3b65
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/groups.go
@@ -0,0 +1,293 @@
+package 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
+}
diff --git a/vendor/github.com/nlopes/slack/history.go b/vendor/github.com/nlopes/slack/history.go
new file mode 100644
index 00000000..87b2e1ed
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/history.go
@@ -0,0 +1,36 @@
+package 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,
+ }
+}
diff --git a/vendor/github.com/nlopes/slack/im.go b/vendor/github.com/nlopes/slack/im.go
new file mode 100644
index 00000000..6fbc39e9
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/im.go
@@ -0,0 +1,130 @@
+package 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
+}
diff --git a/vendor/github.com/nlopes/slack/info.go b/vendor/github.com/nlopes/slack/info.go
new file mode 100644
index 00000000..5f0e5414
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/info.go
@@ -0,0 +1,206 @@
+package 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 {
+ Image48 string `json:"image_48"`
+}
+
+// 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"`
+}
+
+// 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
+}
diff --git a/vendor/github.com/nlopes/slack/item.go b/vendor/github.com/nlopes/slack/item.go
new file mode 100644
index 00000000..89af4eb1
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/item.go
@@ -0,0 +1,75 @@
+package 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}
+}
diff --git a/vendor/github.com/nlopes/slack/messageID.go b/vendor/github.com/nlopes/slack/messageID.go
new file mode 100644
index 00000000..a17472b4
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/messageID.go
@@ -0,0 +1,30 @@
+package 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
+}
diff --git a/vendor/github.com/nlopes/slack/messages.go b/vendor/github.com/nlopes/slack/messages.go
new file mode 100644
index 00000000..3433e06b
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/messages.go
@@ -0,0 +1,131 @@
+package 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"`
+}
+
+// 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"`
+ 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"`
+
+ // 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"`
+}
+
+// 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,
+ }
+}
diff --git a/vendor/github.com/nlopes/slack/misc.go b/vendor/github.com/nlopes/slack/misc.go
new file mode 100644
index 00000000..c0f023cb
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/misc.go
@@ -0,0 +1,119 @@
+package slack
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+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, fpath string, values url.Values) (*http.Request, error) {
+ fullpath, err := filepath.Abs(fpath)
+ if err != nil {
+ return nil, err
+ }
+ file, err := os.Open(fullpath)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ body := &bytes.Buffer{}
+ wr := multipart.NewWriter(body)
+
+ ioWriter, err := wr.CreateFormFile("file", filepath.Base(fullpath))
+ if err != nil {
+ wr.Close()
+ return nil, err
+ }
+ bytes, err := io.Copy(ioWriter, file)
+ if err != nil {
+ wr.Close()
+ return nil, err
+ }
+ // Close the multipart writer or the footer won't be written
+ wr.Close()
+ stat, err := file.Stat()
+ if err != nil {
+ return nil, err
+ }
+ if bytes != stat.Size() {
+ return nil, errors.New("could not read the whole file")
+ }
+ 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 postWithMultipartResponse(path string, filepath string, values url.Values, intf interface{}, debug bool) error {
+ req, err := fileUploadReq(SLACK_API+path, filepath, values)
+ resp, err := HTTPClient.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ return parseResponseBody(resp.Body, &intf, debug)
+}
+
+func postForm(endpoint string, values url.Values, intf interface{}, debug bool) error {
+ resp, err := HTTPClient.PostForm(endpoint, values)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ 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)
+}
diff --git a/vendor/github.com/nlopes/slack/oauth.go b/vendor/github.com/nlopes/slack/oauth.go
new file mode 100644
index 00000000..33c3ed2d
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/oauth.go
@@ -0,0 +1,54 @@
+package slack
+
+import (
+ "errors"
+ "net/url"
+)
+
+type OAuthResponseIncomingWebhook struct {
+ URL string `json:"url"`
+ Channel string `json:"channel"`
+ 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"`
+ 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
+}
diff --git a/vendor/github.com/nlopes/slack/pagination.go b/vendor/github.com/nlopes/slack/pagination.go
new file mode 100644
index 00000000..87dd136a
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/pagination.go
@@ -0,0 +1,20 @@
+package 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"`
+}
diff --git a/vendor/github.com/nlopes/slack/pins.go b/vendor/github.com/nlopes/slack/pins.go
new file mode 100644
index 00000000..b95efbb6
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/pins.go
@@ -0,0 +1,79 @@
+package 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
+}
diff --git a/vendor/github.com/nlopes/slack/reactions.go b/vendor/github.com/nlopes/slack/reactions.go
new file mode 100644
index 00000000..8769543d
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/reactions.go
@@ -0,0 +1,246 @@
+package 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
+}
diff --git a/vendor/github.com/nlopes/slack/rtm.go b/vendor/github.com/nlopes/slack/rtm.go
new file mode 100644
index 00000000..f3552c53
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/rtm.go
@@ -0,0 +1,39 @@
+package slack
+
+import (
+ "fmt"
+ "net/url"
+)
+
+// 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
+}
+
+// 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 newRTM(api)
+}
diff --git a/vendor/github.com/nlopes/slack/search.go b/vendor/github.com/nlopes/slack/search.go
new file mode 100644
index 00000000..ab3c5dad
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/search.go
@@ -0,0 +1,137 @@
+package 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
+}
diff --git a/vendor/github.com/nlopes/slack/slack.go b/vendor/github.com/nlopes/slack/slack.go
new file mode 100644
index 00000000..eb686354
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/slack.go
@@ -0,0 +1,88 @@
+package 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...)
+ }
+}
diff --git a/vendor/github.com/nlopes/slack/stars.go b/vendor/github.com/nlopes/slack/stars.go
new file mode 100644
index 00000000..cc12e6ec
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/stars.go
@@ -0,0 +1,135 @@
+package 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
+}
diff --git a/vendor/github.com/nlopes/slack/team.go b/vendor/github.com/nlopes/slack/team.go
new file mode 100644
index 00000000..41507f68
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/team.go
@@ -0,0 +1,46 @@
+package slack
+
+import (
+ "errors"
+ "net/url"
+)
+
+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"`
+}
+
+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
+}
+
+// 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
+}
diff --git a/vendor/github.com/nlopes/slack/users.go b/vendor/github.com/nlopes/slack/users.go
new file mode 100644
index 00000000..a0ed81e6
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/users.go
@@ -0,0 +1,140 @@
+package slack
+
+import (
+ "errors"
+ "net/url"
+)
+
+// 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"`
+}
+
+// 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 userResponseFull struct {
+ Members []User `json:"members,omitempty"` // ListUsers
+ User `json:"user,omitempty"` // GetUserInfo
+ UserPresence // GetUserPresence
+ SlackResponse
+}
+
+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 retrive 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
+
+}
diff --git a/vendor/github.com/nlopes/slack/websocket.go b/vendor/github.com/nlopes/slack/websocket.go
new file mode 100644
index 00000000..6eb09263
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket.go
@@ -0,0 +1,93 @@
+package 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().
+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
+}
+
+// NewRTM returns a RTM, which provides a fully managed connection to
+// Slack's websocket-based Real-Time Messaging protocol.
+func newRTM(api *Client) *RTM {
+ return &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),
+ }
+}
+
+// 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
+}
diff --git a/vendor/github.com/nlopes/slack/websocket_channels.go b/vendor/github.com/nlopes/slack/websocket_channels.go
new file mode 100644
index 00000000..7dd3319b
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_channels.go
@@ -0,0 +1,72 @@
+package 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
diff --git a/vendor/github.com/nlopes/slack/websocket_dm.go b/vendor/github.com/nlopes/slack/websocket_dm.go
new file mode 100644
index 00000000..98bf6f88
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_dm.go
@@ -0,0 +1,23 @@
+package 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
diff --git a/vendor/github.com/nlopes/slack/websocket_dnd.go b/vendor/github.com/nlopes/slack/websocket_dnd.go
new file mode 100644
index 00000000..62ddea3a
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_dnd.go
@@ -0,0 +1,8 @@
+package 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"`
+}
diff --git a/vendor/github.com/nlopes/slack/websocket_files.go b/vendor/github.com/nlopes/slack/websocket_files.go
new file mode 100644
index 00000000..8c5bd4f8
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_files.go
@@ -0,0 +1,49 @@
+package 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"`
+}
diff --git a/vendor/github.com/nlopes/slack/websocket_groups.go b/vendor/github.com/nlopes/slack/websocket_groups.go
new file mode 100644
index 00000000..eb88985c
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_groups.go
@@ -0,0 +1,49 @@
+package 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
diff --git a/vendor/github.com/nlopes/slack/websocket_internals.go b/vendor/github.com/nlopes/slack/websocket_internals.go
new file mode 100644
index 00000000..2a8abe6e
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_internals.go
@@ -0,0 +1,92 @@
+package 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()
+}
diff --git a/vendor/github.com/nlopes/slack/websocket_managed_conn.go b/vendor/github.com/nlopes/slack/websocket_managed_conn.go
new file mode 100644
index 00000000..db2df902
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_managed_conn.go
@@ -0,0 +1,427 @@
+package 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)
+ // 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.
+func (rtm *RTM) connect(connectionCount int) (*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()
+ 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 attemps to connect to the slack websocket. It returns the
+// full information returned by the "rtm.start" method on the slack API.
+func (rtm *RTM) startRTMAndDial() (*Info, *websocket.Conn, error) {
+ info, url, err := rtm.StartRTM()
+ 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()
+ }
+ }
+}
+
+// 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
+ }
+ err := websocket.JSON.Send(rtm.conn, msg)
+ if 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"}
+ err := websocket.JSON.Send(rtm.conn, msg)
+ if 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{},
+}
diff --git a/vendor/github.com/nlopes/slack/websocket_misc.go b/vendor/github.com/nlopes/slack/websocket_misc.go
new file mode 100644
index 00000000..6c2c6abf
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_misc.go
@@ -0,0 +1,117 @@
+package 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"`
+ 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"`
+}
diff --git a/vendor/github.com/nlopes/slack/websocket_pins.go b/vendor/github.com/nlopes/slack/websocket_pins.go
new file mode 100644
index 00000000..95445e28
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_pins.go
@@ -0,0 +1,16 @@
+package 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
diff --git a/vendor/github.com/nlopes/slack/websocket_proxy.go b/vendor/github.com/nlopes/slack/websocket_proxy.go
new file mode 100644
index 00000000..440015d6
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_proxy.go
@@ -0,0 +1,83 @@
+package slack
+
+import (
+ "crypto/tls"
+ "errors"
+ "net"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "os"
+ "strings"
+
+ "golang.org/x/net/websocket"
+)
+
+// Taken and reworked from: https://gist.github.com/madmo/8548738
+func websocketHTTPConnect(proxy, urlString string) (net.Conn, error) {
+ p, err := net.Dial("tcp", proxy)
+ if err != nil {
+ return nil, err
+ }
+
+ turl, err := url.Parse(urlString)
+ if err != nil {
+ return nil, err
+ }
+
+ req := http.Request{
+ Method: "CONNECT",
+ URL: &url.URL{},
+ Host: turl.Host,
+ }
+
+ cc := httputil.NewProxyClientConn(p, nil)
+ 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)
+}
diff --git a/vendor/github.com/nlopes/slack/websocket_reactions.go b/vendor/github.com/nlopes/slack/websocket_reactions.go
new file mode 100644
index 00000000..e4973878
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_reactions.go
@@ -0,0 +1,25 @@
+package 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
diff --git a/vendor/github.com/nlopes/slack/websocket_stars.go b/vendor/github.com/nlopes/slack/websocket_stars.go
new file mode 100644
index 00000000..e0f2dda3
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_stars.go
@@ -0,0 +1,14 @@
+package 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
diff --git a/vendor/github.com/nlopes/slack/websocket_teams.go b/vendor/github.com/nlopes/slack/websocket_teams.go
new file mode 100644
index 00000000..3898c833
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_teams.go
@@ -0,0 +1,33 @@
+package 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"`
+}
diff --git a/vendor/github.com/nlopes/slack/websocket_utils.go b/vendor/github.com/nlopes/slack/websocket_utils.go
new file mode 100644
index 00000000..b3d0ec89
--- /dev/null
+++ b/vendor/github.com/nlopes/slack/websocket_utils.go
@@ -0,0 +1,20 @@
+package 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
+}
diff --git a/vendor/golang.org/x/net/websocket/LICENSE b/vendor/golang.org/x/net/websocket/LICENSE
new file mode 100644
index 00000000..6a66aea5
--- /dev/null
+++ b/vendor/golang.org/x/net/websocket/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+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
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/golang.org/x/net/websocket/client.go b/vendor/golang.org/x/net/websocket/client.go
new file mode 100644
index 00000000..20d1e1e3
--- /dev/null
+++ b/vendor/golang.org/x/net/websocket/client.go
@@ -0,0 +1,113 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package websocket
+
+import (
+ "bufio"
+ "crypto/tls"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+)
+
+// DialError is an error that occurs while dialling a websocket server.
+type DialError struct {
+ *Config
+ Err error
+}
+
+func (e *DialError) Error() string {
+ return "websocket.Dial " + e.Config.Location.String() + ": " + e.Err.Error()
+}
+
+// NewConfig creates a new WebSocket config for client connection.
+func NewConfig(server, origin string) (config *Config, err error) {
+ config = new(Config)
+ config.Version = ProtocolVersionHybi13
+ config.Location, err = url.ParseRequestURI(server)
+ if err != nil {
+ return
+ }
+ config.Origin, err = url.ParseRequestURI(origin)
+ if err != nil {
+ return
+ }
+ config.Header = http.Header(make(map[string][]string))
+ return
+}
+
+// NewClient creates a new WebSocket client connection over rwc.
+func NewClient(config *Config, rwc io.ReadWriteCloser) (ws *Conn, err error) {
+ br := bufio.NewReader(rwc)
+ bw := bufio.NewWriter(rwc)
+ err = hybiClientHandshake(config, br, bw)
+ if err != nil {
+ return
+ }
+ buf := bufio.NewReadWriter(br, bw)
+ ws = newHybiClientConn(config, buf, rwc)
+ return
+}
+
+// Dial opens a new client connection to a WebSocket.
+func Dial(url_, protocol, origin string) (ws *Conn, err error) {
+ config, err := NewConfig(url_, origin)
+ if err != nil {
+ return nil, err
+ }
+ if protocol != "" {
+ config.Protocol = []string{protocol}
+ }
+ return DialConfig(config)
+}
+
+var portMap = map[string]string{
+ "ws": "80",
+ "wss": "443",
+}
+
+func parseAuthority(location *url.URL) string {
+ if _, ok := portMap[location.Scheme]; ok {
+ if _, _, err := net.SplitHostPort(location.Host); err != nil {
+ return net.JoinHostPort(location.Host, portMap[location.Scheme])
+ }
+ }
+ return location.Host
+}
+
+// DialConfig opens a new client connection to a WebSocket with a config.
+func DialConfig(config *Config) (ws *Conn, err error) {
+ var client net.Conn
+ if config.Location == nil {
+ return nil, &DialError{config, ErrBadWebSocketLocation}
+ }
+ if config.Origin == nil {
+ return nil, &DialError{config, ErrBadWebSocketOrigin}
+ }
+ switch config.Location.Scheme {
+ case "ws":
+ client, err = net.Dial("tcp", parseAuthority(config.Location))
+
+ case "wss":
+ client, err = tls.Dial("tcp", parseAuthority(config.Location), config.TlsConfig)
+
+ default:
+ err = ErrBadScheme
+ }
+ if err != nil {
+ goto Error
+ }
+
+ ws, err = NewClient(config, client)
+ if err != nil {
+ client.Close()
+ goto Error
+ }
+ return
+
+Error:
+ return nil, &DialError{config, err}
+}
diff --git a/vendor/golang.org/x/net/websocket/hybi.go b/vendor/golang.org/x/net/websocket/hybi.go
new file mode 100644
index 00000000..8cffdd16
--- /dev/null
+++ b/vendor/golang.org/x/net/websocket/hybi.go
@@ -0,0 +1,583 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package websocket
+
+// This file implements a protocol of hybi draft.
+// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/rand"
+ "crypto/sha1"
+ "encoding/base64"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+const (
+ websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+
+ closeStatusNormal = 1000
+ closeStatusGoingAway = 1001
+ closeStatusProtocolError = 1002
+ closeStatusUnsupportedData = 1003
+ closeStatusFrameTooLarge = 1004
+ closeStatusNoStatusRcvd = 1005
+ closeStatusAbnormalClosure = 1006
+ closeStatusBadMessageData = 1007
+ closeStatusPolicyViolation = 1008
+ closeStatusTooBigData = 1009
+ closeStatusExtensionMismatch = 1010
+
+ maxControlFramePayloadLength = 125
+)
+
+var (
+ ErrBadMaskingKey = &ProtocolError{"bad masking key"}
+ ErrBadPongMessage = &ProtocolError{"bad pong message"}
+ ErrBadClosingStatus = &ProtocolError{"bad closing status"}
+ ErrUnsupportedExtensions = &ProtocolError{"unsupported extensions"}
+ ErrNotImplemented = &ProtocolError{"not implemented"}
+
+ handshakeHeader = map[string]bool{
+ "Host": true,
+ "Upgrade": true,
+ "Connection": true,
+ "Sec-Websocket-Key": true,
+ "Sec-Websocket-Origin": true,
+ "Sec-Websocket-Version": true,
+ "Sec-Websocket-Protocol": true,
+ "Sec-Websocket-Accept": true,
+ }
+)
+
+// A hybiFrameHeader is a frame header as defined in hybi draft.
+type hybiFrameHeader struct {
+ Fin bool
+ Rsv [3]bool
+ OpCode byte
+ Length int64
+ MaskingKey []byte
+
+ data *bytes.Buffer
+}
+
+// A hybiFrameReader is a reader for hybi frame.
+type hybiFrameReader struct {
+ reader io.Reader
+
+ header hybiFrameHeader
+ pos int64
+ length int
+}
+
+func (frame *hybiFrameReader) Read(msg []byte) (n int, err error) {
+ n, err = frame.reader.Read(msg)
+ if frame.header.MaskingKey != nil {
+ for i := 0; i < n; i++ {
+ msg[i] = msg[i] ^ frame.header.MaskingKey[frame.pos%4]
+ frame.pos++
+ }
+ }
+ return n, err
+}
+
+func (frame *hybiFrameReader) PayloadType() byte { return frame.header.OpCode }
+
+func (frame *hybiFrameReader) HeaderReader() io.Reader {
+ if frame.header.data == nil {
+ return nil
+ }
+ if frame.header.data.Len() == 0 {
+ return nil
+ }
+ return frame.header.data
+}
+
+func (frame *hybiFrameReader) TrailerReader() io.Reader { return nil }
+
+func (frame *hybiFrameReader) Len() (n int) { return frame.length }
+
+// A hybiFrameReaderFactory creates new frame reader based on its frame type.
+type hybiFrameReaderFactory struct {
+ *bufio.Reader
+}
+
+// NewFrameReader reads a frame header from the connection, and creates new reader for the frame.
+// See Section 5.2 Base Framing protocol for detail.
+// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17#section-5.2
+func (buf hybiFrameReaderFactory) NewFrameReader() (frame frameReader, err error) {
+ hybiFrame := new(hybiFrameReader)
+ frame = hybiFrame
+ var header []byte
+ var b byte
+ // First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits)
+ b, err = buf.ReadByte()
+ if err != nil {
+ return
+ }
+ header = append(header, b)
+ hybiFrame.header.Fin = ((header[0] >> 7) & 1) != 0
+ for i := 0; i < 3; i++ {
+ j := uint(6 - i)
+ hybiFrame.header.Rsv[i] = ((header[0] >> j) & 1) != 0
+ }
+ hybiFrame.header.OpCode = header[0] & 0x0f
+
+ // Second byte. Mask/Payload len(7bits)
+ b, err = buf.ReadByte()
+ if err != nil {
+ return
+ }
+ header = append(header, b)
+ mask := (b & 0x80) != 0
+ b &= 0x7f
+ lengthFields := 0
+ switch {
+ case b <= 125: // Payload length 7bits.
+ hybiFrame.header.Length = int64(b)
+ case b == 126: // Payload length 7+16bits
+ lengthFields = 2
+ case b == 127: // Payload length 7+64bits
+ lengthFields = 8
+ }
+ for i := 0; i < lengthFields; i++ {
+ b, err = buf.ReadByte()
+ if err != nil {
+ return
+ }
+ if lengthFields == 8 && i == 0 { // MSB must be zero when 7+64 bits
+ b &= 0x7f
+ }
+ header = append(header, b)
+ hybiFrame.header.Length = hybiFrame.header.Length*256 + int64(b)
+ }
+ if mask {
+ // Masking key. 4 bytes.
+ for i := 0; i < 4; i++ {
+ b, err = buf.ReadByte()
+ if err != nil {
+ return
+ }
+ header = append(header, b)
+ hybiFrame.header.MaskingKey = append(hybiFrame.header.MaskingKey, b)
+ }
+ }
+ hybiFrame.reader = io.LimitReader(buf.Reader, hybiFrame.header.Length)
+ hybiFrame.header.data = bytes.NewBuffer(header)
+ hybiFrame.length = len(header) + int(hybiFrame.header.Length)
+ return
+}
+
+// A HybiFrameWriter is a writer for hybi frame.
+type hybiFrameWriter struct {
+ writer *bufio.Writer
+
+ header *hybiFrameHeader
+}
+
+func (frame *hybiFrameWriter) Write(msg []byte) (n int, err error) {
+ var header []byte
+ var b byte
+ if frame.header.Fin {
+ b |= 0x80
+ }
+ for i := 0; i < 3; i++ {
+ if frame.header.Rsv[i] {
+ j := uint(6 - i)
+ b |= 1 << j
+ }
+ }
+ b |= frame.header.OpCode
+ header = append(header, b)
+ if frame.header.MaskingKey != nil {
+ b = 0x80
+ } else {
+ b = 0
+ }
+ lengthFields := 0
+ length := len(msg)
+ switch {
+ case length <= 125:
+ b |= byte(length)
+ case length < 65536:
+ b |= 126
+ lengthFields = 2
+ default:
+ b |= 127
+ lengthFields = 8
+ }
+ header = append(header, b)
+ for i := 0; i < lengthFields; i++ {
+ j := uint((lengthFields - i - 1) * 8)
+ b = byte((length >> j) & 0xff)
+ header = append(header, b)
+ }
+ if frame.header.MaskingKey != nil {
+ if len(frame.header.MaskingKey) != 4 {
+ return 0, ErrBadMaskingKey
+ }
+ header = append(header, frame.header.MaskingKey...)
+ frame.writer.Write(header)
+ data := make([]byte, length)
+ for i := range data {
+ data[i] = msg[i] ^ frame.header.MaskingKey[i%4]
+ }
+ frame.writer.Write(data)
+ err = frame.writer.Flush()
+ return length, err
+ }
+ frame.writer.Write(header)
+ frame.writer.Write(msg)
+ err = frame.writer.Flush()
+ return length, err
+}
+
+func (frame *hybiFrameWriter) Close() error { return nil }
+
+type hybiFrameWriterFactory struct {
+ *bufio.Writer
+ needMaskingKey bool
+}
+
+func (buf hybiFrameWriterFactory) NewFrameWriter(payloadType byte) (frame frameWriter, err error) {
+ frameHeader := &hybiFrameHeader{Fin: true, OpCode: payloadType}
+ if buf.needMaskingKey {
+ frameHeader.MaskingKey, err = generateMaskingKey()
+ if err != nil {
+ return nil, err
+ }
+ }
+ return &hybiFrameWriter{writer: buf.Writer, header: frameHeader}, nil
+}
+
+type hybiFrameHandler struct {
+ conn *Conn
+ payloadType byte
+}
+
+func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (frameReader, error) {
+ if handler.conn.IsServerConn() {
+ // The client MUST mask all frames sent to the server.
+ if frame.(*hybiFrameReader).header.MaskingKey == nil {
+ handler.WriteClose(closeStatusProtocolError)
+ return nil, io.EOF
+ }
+ } else {
+ // The server MUST NOT mask all frames.
+ if frame.(*hybiFrameReader).header.MaskingKey != nil {
+ handler.WriteClose(closeStatusProtocolError)
+ return nil, io.EOF
+ }
+ }
+ if header := frame.HeaderReader(); header != nil {
+ io.Copy(ioutil.Discard, header)
+ }
+ switch frame.PayloadType() {
+ case ContinuationFrame:
+ frame.(*hybiFrameReader).header.OpCode = handler.payloadType
+ case TextFrame, BinaryFrame:
+ handler.payloadType = frame.PayloadType()
+ case CloseFrame:
+ return nil, io.EOF
+ case PingFrame, PongFrame:
+ b := make([]byte, maxControlFramePayloadLength)
+ n, err := io.ReadFull(frame, b)
+ if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
+ return nil, err
+ }
+ io.Copy(ioutil.Discard, frame)
+ if frame.PayloadType() == PingFrame {
+ if _, err := handler.WritePong(b[:n]); err != nil {
+ return nil, err
+ }
+ }
+ return nil, nil
+ }
+ return frame, nil
+}
+
+func (handler *hybiFrameHandler) WriteClose(status int) (err error) {
+ handler.conn.wio.Lock()
+ defer handler.conn.wio.Unlock()
+ w, err := handler.conn.frameWriterFactory.NewFrameWriter(CloseFrame)
+ if err != nil {
+ return err
+ }
+ msg := make([]byte, 2)
+ binary.BigEndian.PutUint16(msg, uint16(status))
+ _, err = w.Write(msg)
+ w.Close()
+ return err
+}
+
+func (handler *hybiFrameHandler) WritePong(msg []byte) (n int, err error) {
+ handler.conn.wio.Lock()
+ defer handler.conn.wio.Unlock()
+ w, err := handler.conn.frameWriterFactory.NewFrameWriter(PongFrame)
+ if err != nil {
+ return 0, err
+ }
+ n, err = w.Write(msg)
+ w.Close()
+ return n, err
+}
+
+// newHybiConn creates a new WebSocket connection speaking hybi draft protocol.
+func newHybiConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn {
+ if buf == nil {
+ br := bufio.NewReader(rwc)
+ bw := bufio.NewWriter(rwc)
+ buf = bufio.NewReadWriter(br, bw)
+ }
+ ws := &Conn{config: config, request: request, buf: buf, rwc: rwc,
+ frameReaderFactory: hybiFrameReaderFactory{buf.Reader},
+ frameWriterFactory: hybiFrameWriterFactory{
+ buf.Writer, request == nil},
+ PayloadType: TextFrame,
+ defaultCloseStatus: closeStatusNormal}
+ ws.frameHandler = &hybiFrameHandler{conn: ws}
+ return ws
+}
+
+// generateMaskingKey generates a masking key for a frame.
+func generateMaskingKey() (maskingKey []byte, err error) {
+ maskingKey = make([]byte, 4)
+ if _, err = io.ReadFull(rand.Reader, maskingKey); err != nil {
+ return
+ }
+ return
+}
+
+// generateNonce generates a nonce consisting of a randomly selected 16-byte
+// value that has been base64-encoded.
+func generateNonce() (nonce []byte) {
+ key := make([]byte, 16)
+ if _, err := io.ReadFull(rand.Reader, key); err != nil {
+ panic(err)
+ }
+ nonce = make([]byte, 24)
+ base64.StdEncoding.Encode(nonce, key)
+ return
+}
+
+// removeZone removes IPv6 zone identifer from host.
+// E.g., "[fe80::1%en0]:8080" to "[fe80::1]:8080"
+func removeZone(host string) string {
+ if !strings.HasPrefix(host, "[") {
+ return host
+ }
+ i := strings.LastIndex(host, "]")
+ if i < 0 {
+ return host
+ }
+ j := strings.LastIndex(host[:i], "%")
+ if j < 0 {
+ return host
+ }
+ return host[:j] + host[i:]
+}
+
+// getNonceAccept computes the base64-encoded SHA-1 of the concatenation of
+// the nonce ("Sec-WebSocket-Key" value) with the websocket GUID string.
+func getNonceAccept(nonce []byte) (expected []byte, err error) {
+ h := sha1.New()
+ if _, err = h.Write(nonce); err != nil {
+ return
+ }
+ if _, err = h.Write([]byte(websocketGUID)); err != nil {
+ return
+ }
+ expected = make([]byte, 28)
+ base64.StdEncoding.Encode(expected, h.Sum(nil))
+ return
+}
+
+// Client handshake described in draft-ietf-hybi-thewebsocket-protocol-17
+func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) {
+ bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n")
+
+ // According to RFC 6874, an HTTP client, proxy, or other
+ // intermediary must remove any IPv6 zone identifier attached
+ // to an outgoing URI.
+ bw.WriteString("Host: " + removeZone(config.Location.Host) + "\r\n")
+ bw.WriteString("Upgrade: websocket\r\n")
+ bw.WriteString("Connection: Upgrade\r\n")
+ nonce := generateNonce()
+ if config.handshakeData != nil {
+ nonce = []byte(config.handshakeData["key"])
+ }
+ bw.WriteString("Sec-WebSocket-Key: " + string(nonce) + "\r\n")
+ bw.WriteString("Origin: " + strings.ToLower(config.Origin.String()) + "\r\n")
+
+ if config.Version != ProtocolVersionHybi13 {
+ return ErrBadProtocolVersion
+ }
+
+ bw.WriteString("Sec-WebSocket-Version: " + fmt.Sprintf("%d", config.Version) + "\r\n")
+ if len(config.Protocol) > 0 {
+ bw.WriteString("Sec-WebSocket-Protocol: " + strings.Join(config.Protocol, ", ") + "\r\n")
+ }
+ // TODO(ukai): send Sec-WebSocket-Extensions.
+ err = config.Header.WriteSubset(bw, handshakeHeader)
+ if err != nil {
+ return err
+ }
+
+ bw.WriteString("\r\n")
+ if err = bw.Flush(); err != nil {
+ return err
+ }
+
+ resp, err := http.ReadResponse(br, &http.Request{Method: "GET"})
+ if err != nil {
+ return err
+ }
+ if resp.StatusCode != 101 {
+ return ErrBadStatus
+ }
+ if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" ||
+ strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
+ return ErrBadUpgrade
+ }
+ expectedAccept, err := getNonceAccept(nonce)
+ if err != nil {
+ return err
+ }
+ if resp.Header.Get("Sec-WebSocket-Accept") != string(expectedAccept) {
+ return ErrChallengeResponse
+ }
+ if resp.Header.Get("Sec-WebSocket-Extensions") != "" {
+ return ErrUnsupportedExtensions
+ }
+ offeredProtocol := resp.Header.Get("Sec-WebSocket-Protocol")
+ if offeredProtocol != "" {
+ protocolMatched := false
+ for i := 0; i < len(config.Protocol); i++ {
+ if config.Protocol[i] == offeredProtocol {
+ protocolMatched = true
+ break
+ }
+ }
+ if !protocolMatched {
+ return ErrBadWebSocketProtocol
+ }
+ config.Protocol = []string{offeredProtocol}
+ }
+
+ return nil
+}
+
+// newHybiClientConn creates a client WebSocket connection after handshake.
+func newHybiClientConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser) *Conn {
+ return newHybiConn(config, buf, rwc, nil)
+}
+
+// A HybiServerHandshaker performs a server handshake using hybi draft protocol.
+type hybiServerHandshaker struct {
+ *Config
+ accept []byte
+}
+
+func (c *hybiServerHandshaker) ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) {
+ c.Version = ProtocolVersionHybi13
+ if req.Method != "GET" {
+ return http.StatusMethodNotAllowed, ErrBadRequestMethod
+ }
+ // HTTP version can be safely ignored.
+
+ if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" ||
+ !strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") {
+ return http.StatusBadRequest, ErrNotWebSocket
+ }
+
+ key := req.Header.Get("Sec-Websocket-Key")
+ if key == "" {
+ return http.StatusBadRequest, ErrChallengeResponse
+ }
+ version := req.Header.Get("Sec-Websocket-Version")
+ switch version {
+ case "13":
+ c.Version = ProtocolVersionHybi13
+ default:
+ return http.StatusBadRequest, ErrBadWebSocketVersion
+ }
+ var scheme string
+ if req.TLS != nil {
+ scheme = "wss"
+ } else {
+ scheme = "ws"
+ }
+ c.Location, err = url.ParseRequestURI(scheme + "://" + req.Host + req.URL.RequestURI())
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ protocol := strings.TrimSpace(req.Header.Get("Sec-Websocket-Protocol"))
+ if protocol != "" {
+ protocols := strings.Split(protocol, ",")
+ for i := 0; i < len(protocols); i++ {
+ c.Protocol = append(c.Protocol, strings.TrimSpace(protocols[i]))
+ }
+ }
+ c.accept, err = getNonceAccept([]byte(key))
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusSwitchingProtocols, nil
+}
+
+// Origin parses the Origin header in req.
+// If the Origin header is not set, it returns nil and nil.
+func Origin(config *Config, req *http.Request) (*url.URL, error) {
+ var origin string
+ switch config.Version {
+ case ProtocolVersionHybi13:
+ origin = req.Header.Get("Origin")
+ }
+ if origin == "" {
+ return nil, nil
+ }
+ return url.ParseRequestURI(origin)
+}
+
+func (c *hybiServerHandshaker) AcceptHandshake(buf *bufio.Writer) (err error) {
+ if len(c.Protocol) > 0 {
+ if len(c.Protocol) != 1 {
+ // You need choose a Protocol in Handshake func in Server.
+ return ErrBadWebSocketProtocol
+ }
+ }
+ buf.WriteString("HTTP/1.1 101 Switching Protocols\r\n")
+ buf.WriteString("Upgrade: websocket\r\n")
+ buf.WriteString("Connection: Upgrade\r\n")
+ buf.WriteString("Sec-WebSocket-Accept: " + string(c.accept) + "\r\n")
+ if len(c.Protocol) > 0 {
+ buf.WriteString("Sec-WebSocket-Protocol: " + c.Protocol[0] + "\r\n")
+ }
+ // TODO(ukai): send Sec-WebSocket-Extensions.
+ if c.Header != nil {
+ err := c.Header.WriteSubset(buf, handshakeHeader)
+ if err != nil {
+ return err
+ }
+ }
+ buf.WriteString("\r\n")
+ return buf.Flush()
+}
+
+func (c *hybiServerHandshaker) NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn {
+ return newHybiServerConn(c.Config, buf, rwc, request)
+}
+
+// newHybiServerConn returns a new WebSocket connection speaking hybi draft protocol.
+func newHybiServerConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn {
+ return newHybiConn(config, buf, rwc, request)
+}
diff --git a/vendor/golang.org/x/net/websocket/server.go b/vendor/golang.org/x/net/websocket/server.go
new file mode 100644
index 00000000..0895dea1
--- /dev/null
+++ b/vendor/golang.org/x/net/websocket/server.go
@@ -0,0 +1,113 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package websocket
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "net/http"
+)
+
+func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Request, config *Config, handshake func(*Config, *http.Request) error) (conn *Conn, err error) {
+ var hs serverHandshaker = &hybiServerHandshaker{Config: config}
+ code, err := hs.ReadHandshake(buf.Reader, req)
+ if err == ErrBadWebSocketVersion {
+ fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
+ fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\r\n", SupportedProtocolVersion)
+ buf.WriteString("\r\n")
+ buf.WriteString(err.Error())
+ buf.Flush()
+ return
+ }
+ if err != nil {
+ fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
+ buf.WriteString("\r\n")
+ buf.WriteString(err.Error())
+ buf.Flush()
+ return
+ }
+ if handshake != nil {
+ err = handshake(config, req)
+ if err != nil {
+ code = http.StatusForbidden
+ fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
+ buf.WriteString("\r\n")
+ buf.Flush()
+ return
+ }
+ }
+ err = hs.AcceptHandshake(buf.Writer)
+ if err != nil {
+ code = http.StatusBadRequest
+ fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
+ buf.WriteString("\r\n")
+ buf.Flush()
+ return
+ }
+ conn = hs.NewServerConn(buf, rwc, req)
+ return
+}
+
+// Server represents a server of a WebSocket.
+type Server struct {
+ // Config is a WebSocket configuration for new WebSocket connection.
+ Config
+
+ // Handshake is an optional function in WebSocket handshake.
+ // For example, you can check, or don't check Origin header.
+ // Another example, you can select config.Protocol.
+ Handshake func(*Config, *http.Request) error
+
+ // Handler handles a WebSocket connection.
+ Handler
+}
+
+// ServeHTTP implements the http.Handler interface for a WebSocket
+func (s Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+ s.serveWebSocket(w, req)
+}
+
+func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) {
+ rwc, buf, err := w.(http.Hijacker).Hijack()
+ if err != nil {
+ panic("Hijack failed: " + err.Error())
+ }
+ // The server should abort the WebSocket connection if it finds
+ // the client did not send a handshake that matches with protocol
+ // specification.
+ defer rwc.Close()
+ conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake)
+ if err != nil {
+ return
+ }
+ if conn == nil {
+ panic("unexpected nil conn")
+ }
+ s.Handler(conn)
+}
+
+// Handler is a simple interface to a WebSocket browser client.
+// It checks if Origin header is valid URL by default.
+// You might want to verify websocket.Conn.Config().Origin in the func.
+// If you use Server instead of Handler, you could call websocket.Origin and
+// check the origin in your Handshake func. So, if you want to accept
+// non-browser clients, which do not send an Origin header, set a
+// Server.Handshake that does not check the origin.
+type Handler func(*Conn)
+
+func checkOrigin(config *Config, req *http.Request) (err error) {
+ config.Origin, err = Origin(config, req)
+ if err == nil && config.Origin == nil {
+ return fmt.Errorf("null origin")
+ }
+ return err
+}
+
+// ServeHTTP implements the http.Handler interface for a WebSocket
+func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+ s := Server{Handler: h, Handshake: checkOrigin}
+ s.serveWebSocket(w, req)
+}
diff --git a/vendor/golang.org/x/net/websocket/websocket.go b/vendor/golang.org/x/net/websocket/websocket.go
new file mode 100644
index 00000000..9412191d
--- /dev/null
+++ b/vendor/golang.org/x/net/websocket/websocket.go
@@ -0,0 +1,411 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package websocket implements a client and server for the WebSocket protocol
+// as specified in RFC 6455.
+package websocket // import "golang.org/x/net/websocket"
+
+import (
+ "bufio"
+ "crypto/tls"
+ "encoding/json"
+ "errors"
+ "io"
+ "io/ioutil"
+ "net"
+ "net/http"
+ "net/url"
+ "sync"
+ "time"
+)
+
+const (
+ ProtocolVersionHybi13 = 13
+ ProtocolVersionHybi = ProtocolVersionHybi13
+ SupportedProtocolVersion = "13"
+
+ ContinuationFrame = 0
+ TextFrame = 1
+ BinaryFrame = 2
+ CloseFrame = 8
+ PingFrame = 9
+ PongFrame = 10
+ UnknownFrame = 255
+)
+
+// ProtocolError represents WebSocket protocol errors.
+type ProtocolError struct {
+ ErrorString string
+}
+
+func (err *ProtocolError) Error() string { return err.ErrorString }
+
+var (
+ ErrBadProtocolVersion = &ProtocolError{"bad protocol version"}
+ ErrBadScheme = &ProtocolError{"bad scheme"}
+ ErrBadStatus = &ProtocolError{"bad status"}
+ ErrBadUpgrade = &ProtocolError{"missing or bad upgrade"}
+ ErrBadWebSocketOrigin = &ProtocolError{"missing or bad WebSocket-Origin"}
+ ErrBadWebSocketLocation = &ProtocolError{"missing or bad WebSocket-Location"}
+ ErrBadWebSocketProtocol = &ProtocolError{"missing or bad WebSocket-Protocol"}
+ ErrBadWebSocketVersion = &ProtocolError{"missing or bad WebSocket Version"}
+ ErrChallengeResponse = &ProtocolError{"mismatch challenge/response"}
+ ErrBadFrame = &ProtocolError{"bad frame"}
+ ErrBadFrameBoundary = &ProtocolError{"not on frame boundary"}
+ ErrNotWebSocket = &ProtocolError{"not websocket protocol"}
+ ErrBadRequestMethod = &ProtocolError{"bad method"}
+ ErrNotSupported = &ProtocolError{"not supported"}
+)
+
+// Addr is an implementation of net.Addr for WebSocket.
+type Addr struct {
+ *url.URL
+}
+
+// Network returns the network type for a WebSocket, "websocket".
+func (addr *Addr) Network() string { return "websocket" }
+
+// Config is a WebSocket configuration
+type Config struct {
+ // A WebSocket server address.
+ Location *url.URL
+
+ // A Websocket client origin.
+ Origin *url.URL
+
+ // WebSocket subprotocols.
+ Protocol []string
+
+ // WebSocket protocol version.
+ Version int
+
+ // TLS config for secure WebSocket (wss).
+ TlsConfig *tls.Config
+
+ // Additional header fields to be sent in WebSocket opening handshake.
+ Header http.Header
+
+ handshakeData map[string]string
+}
+
+// serverHandshaker is an interface to handle WebSocket server side handshake.
+type serverHandshaker interface {
+ // ReadHandshake reads handshake request message from client.
+ // Returns http response code and error if any.
+ ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error)
+
+ // AcceptHandshake accepts the client handshake request and sends
+ // handshake response back to client.
+ AcceptHandshake(buf *bufio.Writer) (err error)
+
+ // NewServerConn creates a new WebSocket connection.
+ NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) (conn *Conn)
+}
+
+// frameReader is an interface to read a WebSocket frame.
+type frameReader interface {
+ // Reader is to read payload of the frame.
+ io.Reader
+
+ // PayloadType returns payload type.
+ PayloadType() byte
+
+ // HeaderReader returns a reader to read header of the frame.
+ HeaderReader() io.Reader
+
+ // TrailerReader returns a reader to read trailer of the frame.
+ // If it returns nil, there is no trailer in the frame.
+ TrailerReader() io.Reader
+
+ // Len returns total length of the frame, including header and trailer.
+ Len() int
+}
+
+// frameReaderFactory is an interface to creates new frame reader.
+type frameReaderFactory interface {
+ NewFrameReader() (r frameReader, err error)
+}
+
+// frameWriter is an interface to write a WebSocket frame.
+type frameWriter interface {
+ // Writer is to write payload of the frame.
+ io.WriteCloser
+}
+
+// frameWriterFactory is an interface to create new frame writer.
+type frameWriterFactory interface {
+ NewFrameWriter(payloadType byte) (w frameWriter, err error)
+}
+
+type frameHandler interface {
+ HandleFrame(frame frameReader) (r frameReader, err error)
+ WriteClose(status int) (err error)
+}
+
+// Conn represents a WebSocket connection.
+//
+// Multiple goroutines may invoke methods on a Conn simultaneously.
+type Conn struct {
+ config *Config
+ request *http.Request
+
+ buf *bufio.ReadWriter
+ rwc io.ReadWriteCloser
+
+ rio sync.Mutex
+ frameReaderFactory
+ frameReader
+
+ wio sync.Mutex
+ frameWriterFactory
+
+ frameHandler
+ PayloadType byte
+ defaultCloseStatus int
+}
+
+// Read implements the io.Reader interface:
+// it reads data of a frame from the WebSocket connection.
+// if msg is not large enough for the frame data, it fills the msg and next Read
+// will read the rest of the frame data.
+// it reads Text frame or Binary frame.
+func (ws *Conn) Read(msg []byte) (n int, err error) {
+ ws.rio.Lock()
+ defer ws.rio.Unlock()
+again:
+ if ws.frameReader == nil {
+ frame, err := ws.frameReaderFactory.NewFrameReader()
+ if err != nil {
+ return 0, err
+ }
+ ws.frameReader, err = ws.frameHandler.HandleFrame(frame)
+ if err != nil {
+ return 0, err
+ }
+ if ws.frameReader == nil {
+ goto again
+ }
+ }
+ n, err = ws.frameReader.Read(msg)
+ if err == io.EOF {
+ if trailer := ws.frameReader.TrailerReader(); trailer != nil {
+ io.Copy(ioutil.Discard, trailer)
+ }
+ ws.frameReader = nil
+ goto again
+ }
+ return n, err
+}
+
+// Write implements the io.Writer interface:
+// it writes data as a frame to the WebSocket connection.
+func (ws *Conn) Write(msg []byte) (n int, err error) {
+ ws.wio.Lock()
+ defer ws.wio.Unlock()
+ w, err := ws.frameWriterFactory.NewFrameWriter(ws.PayloadType)
+ if err != nil {
+ return 0, err
+ }
+ n, err = w.Write(msg)
+ w.Close()
+ return n, err
+}
+
+// Close implements the io.Closer interface.
+func (ws *Conn) Close() error {
+ err := ws.frameHandler.WriteClose(ws.defaultCloseStatus)
+ err1 := ws.rwc.Close()
+ if err != nil {
+ return err
+ }
+ return err1
+}
+
+func (ws *Conn) IsClientConn() bool { return ws.request == nil }
+func (ws *Conn) IsServerConn() bool { return ws.request != nil }
+
+// LocalAddr returns the WebSocket Origin for the connection for client, or
+// the WebSocket location for server.
+func (ws *Conn) LocalAddr() net.Addr {
+ if ws.IsClientConn() {
+ return &Addr{ws.config.Origin}
+ }
+ return &Addr{ws.config.Location}
+}
+
+// RemoteAddr returns the WebSocket location for the connection for client, or
+// the Websocket Origin for server.
+func (ws *Conn) RemoteAddr() net.Addr {
+ if ws.IsClientConn() {
+ return &Addr{ws.config.Location}
+ }
+ return &Addr{ws.config.Origin}
+}
+
+var errSetDeadline = errors.New("websocket: cannot set deadline: not using a net.Conn")
+
+// SetDeadline sets the connection's network read & write deadlines.
+func (ws *Conn) SetDeadline(t time.Time) error {
+ if conn, ok := ws.rwc.(net.Conn); ok {
+ return conn.SetDeadline(t)
+ }
+ return errSetDeadline
+}
+
+// SetReadDeadline sets the connection's network read deadline.
+func (ws *Conn) SetReadDeadline(t time.Time) error {
+ if conn, ok := ws.rwc.(net.Conn); ok {
+ return conn.SetReadDeadline(t)
+ }
+ return errSetDeadline
+}
+
+// SetWriteDeadline sets the connection's network write deadline.
+func (ws *Conn) SetWriteDeadline(t time.Time) error {
+ if conn, ok := ws.rwc.(net.Conn); ok {
+ return conn.SetWriteDeadline(t)
+ }
+ return errSetDeadline
+}
+
+// Config returns the WebSocket config.
+func (ws *Conn) Config() *Config { return ws.config }
+
+// Request returns the http request upgraded to the WebSocket.
+// It is nil for client side.
+func (ws *Conn) Request() *http.Request { return ws.request }
+
+// Codec represents a symmetric pair of functions that implement a codec.
+type Codec struct {
+ Marshal func(v interface{}) (data []byte, payloadType byte, err error)
+ Unmarshal func(data []byte, payloadType byte, v interface{}) (err error)
+}
+
+// Send sends v marshaled by cd.Marshal as single frame to ws.
+func (cd Codec) Send(ws *Conn, v interface{}) (err error) {
+ data, payloadType, err := cd.Marshal(v)
+ if err != nil {
+ return err
+ }
+ ws.wio.Lock()
+ defer ws.wio.Unlock()
+ w, err := ws.frameWriterFactory.NewFrameWriter(payloadType)
+ if err != nil {
+ return err
+ }
+ _, err = w.Write(data)
+ w.Close()
+ return err
+}
+
+// Receive receives single frame from ws, unmarshaled by cd.Unmarshal and stores in v.
+func (cd Codec) Receive(ws *Conn, v interface{}) (err error) {
+ ws.rio.Lock()
+ defer ws.rio.Unlock()
+ if ws.frameReader != nil {
+ _, err = io.Copy(ioutil.Discard, ws.frameReader)
+ if err != nil {
+ return err
+ }
+ ws.frameReader = nil
+ }
+again:
+ frame, err := ws.frameReaderFactory.NewFrameReader()
+ if err != nil {
+ return err
+ }
+ frame, err = ws.frameHandler.HandleFrame(frame)
+ if err != nil {
+ return err
+ }
+ if frame == nil {
+ goto again
+ }
+ payloadType := frame.PayloadType()
+ data, err := ioutil.ReadAll(frame)
+ if err != nil {
+ return err
+ }
+ return cd.Unmarshal(data, payloadType, v)
+}
+
+func marshal(v interface{}) (msg []byte, payloadType byte, err error) {
+ switch data := v.(type) {
+ case string:
+ return []byte(data), TextFrame, nil
+ case []byte:
+ return data, BinaryFrame, nil
+ }
+ return nil, UnknownFrame, ErrNotSupported
+}
+
+func unmarshal(msg []byte, payloadType byte, v interface{}) (err error) {
+ switch data := v.(type) {
+ case *string:
+ *data = string(msg)
+ return nil
+ case *[]byte:
+ *data = msg
+ return nil
+ }
+ return ErrNotSupported
+}
+
+/*
+Message is a codec to send/receive text/binary data in a frame on WebSocket connection.
+To send/receive text frame, use string type.
+To send/receive binary frame, use []byte type.
+
+Trivial usage:
+
+ import "websocket"
+
+ // receive text frame
+ var message string
+ websocket.Message.Receive(ws, &message)
+
+ // send text frame
+ message = "hello"
+ websocket.Message.Send(ws, message)
+
+ // receive binary frame
+ var data []byte
+ websocket.Message.Receive(ws, &data)
+
+ // send binary frame
+ data = []byte{0, 1, 2}
+ websocket.Message.Send(ws, data)
+
+*/
+var Message = Codec{marshal, unmarshal}
+
+func jsonMarshal(v interface{}) (msg []byte, payloadType byte, err error) {
+ msg, err = json.Marshal(v)
+ return msg, TextFrame, err
+}
+
+func jsonUnmarshal(msg []byte, payloadType byte, v interface{}) (err error) {
+ return json.Unmarshal(msg, v)
+}
+
+/*
+JSON is a codec to send/receive JSON data in a frame from a WebSocket connection.
+
+Trivial usage:
+
+ import "websocket"
+
+ type T struct {
+ Msg string
+ Count int
+ }
+
+ // receive JSON type T
+ var data T
+ websocket.JSON.Receive(ws, &data)
+
+ // send JSON type T
+ websocket.JSON.Send(ws, data)
+*/
+var JSON = Codec{jsonMarshal, jsonUnmarshal}
diff --git a/vendor/manifest b/vendor/manifest
index dc2f9253..c07f6e1b 100644
--- a/vendor/manifest
+++ b/vendor/manifest
@@ -111,6 +111,14 @@
"notests": true
},
{
+ "importpath": "github.com/nlopes/slack",
+ "repository": "https://github.com/nlopes/slack",
+ "vcs": "git",
+ "revision": "4feee83bb2b31d790977ce727a028c6a542c72c7",
+ "branch": "HEAD",
+ "notests": true
+ },
+ {
"importpath": "github.com/pborman/uuid",
"repository": "https://github.com/pborman/uuid",
"vcs": "",
@@ -187,6 +195,15 @@
"notests": true
},
{
+ "importpath": "golang.org/x/net/websocket",
+ "repository": "https://go.googlesource.com/net",
+ "vcs": "git",
+ "revision": "1358eff22f0dd0c54fc521042cc607f6ff4b531a",
+ "branch": "master",
+ "path": "/websocket",
+ "notests": true
+ },
+ {
"importpath": "gopkg.in/gcfg.v1",
"repository": "https://gopkg.in/gcfg.v1",
"vcs": "",