Add Slack support

This commit is contained in:
Wim 2016-09-05 16:34:37 +02:00
parent e449a97bd0
commit b30e85836e
63 changed files with 6314 additions and 1 deletions

View File

@ -6,6 +6,7 @@ import (
"github.com/42wim/matterbridge/bridge/gitter" "github.com/42wim/matterbridge/bridge/gitter"
"github.com/42wim/matterbridge/bridge/irc" "github.com/42wim/matterbridge/bridge/irc"
"github.com/42wim/matterbridge/bridge/mattermost" "github.com/42wim/matterbridge/bridge/mattermost"
"github.com/42wim/matterbridge/bridge/slack"
"github.com/42wim/matterbridge/bridge/xmpp" "github.com/42wim/matterbridge/bridge/xmpp"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"strings" "strings"
@ -42,6 +43,9 @@ func NewBridge(cfg *config.Config) error {
if cfg.Gitter.Enable { if cfg.Gitter.Enable {
b.Bridges = append(b.Bridges, bgitter.New(cfg, c)) 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 { if len(b.Bridges) < 2 {
log.Fatalf("only %d sections enabled. Need at least 2 sections enabled (eg [IRC] and [mattermost]", len(b.Bridges)) 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["mattermost"] = val.Mattermost
m["xmpp"] = val.Xmpp m["xmpp"] = val.Xmpp
m["gitter"] = val.Gitter m["gitter"] = val.Gitter
m["slack"] = val.Slack
b.Channels = append(b.Channels, m) b.Channels = append(b.Channels, m)
} }
return nil return nil
@ -83,6 +88,7 @@ func (b *Bridge) mapIgnores() {
m["mattermost"] = strings.Fields(b.Config.Mattermost.IgnoreNicks) m["mattermost"] = strings.Fields(b.Config.Mattermost.IgnoreNicks)
m["xmpp"] = strings.Fields(b.Config.Xmpp.IgnoreNicks) m["xmpp"] = strings.Fields(b.Config.Xmpp.IgnoreNicks)
m["gitter"] = strings.Fields(b.Config.Gitter.IgnoreNicks) m["gitter"] = strings.Fields(b.Config.Gitter.IgnoreNicks)
m["slack"] = strings.Fields(b.Config.Slack.IgnoreNicks)
b.ignoreNicks = m b.ignoreNicks = m
} }
@ -105,6 +111,7 @@ func (b *Bridge) handleMessage(msg config.Message, dest Bridger) {
return return
} }
b.modifyMessage(&msg, dest.Name()) b.modifyMessage(&msg, dest.Name())
log.Debugf("sending %#v from %s to %s", msg, msg.Origin, dest.Name())
dest.Send(msg) dest.Send(msg)
} }
} }
@ -138,5 +145,7 @@ func (b *Bridge) modifyMessage(msg *config.Message, dest string) {
setNickFormat(msg, b.Config.Xmpp.RemoteNickFormat) setNickFormat(msg, b.Config.Xmpp.RemoteNickFormat)
case "mattermost": case "mattermost":
setNickFormat(msg, b.Config.Mattermost.RemoteNickFormat) setNickFormat(msg, b.Config.Mattermost.RemoteNickFormat)
case "slack":
setNickFormat(msg, b.Config.Slack.RemoteNickFormat)
} }
} }

View File

@ -35,7 +35,6 @@ type Config struct {
RemoteNickFormat string RemoteNickFormat string
Token string Token string
} }
Mattermost struct { Mattermost struct {
URL string URL string
ShowJoinPart bool ShowJoinPart bool
@ -55,6 +54,19 @@ type Config struct {
NoTLS bool NoTLS bool
Enable 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 { Xmpp struct {
IgnoreNicks string IgnoreNicks string
Jid string Jid string
@ -70,6 +82,7 @@ type Config struct {
Mattermost string Mattermost string
Xmpp string Xmpp string
Gitter string Gitter string
Slack string
} }
General struct { General struct {
GiphyAPIKey string GiphyAPIKey string

180
bridge/slack/slack.go Normal file
View File

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

View File

@ -27,6 +27,8 @@ type OMessage struct {
// IMessage for mattermost outgoing webhook. (received from mattermost) // IMessage for mattermost outgoing webhook. (received from mattermost)
type IMessage struct { type IMessage struct {
BotID string `schema:"bot_id"`
BotName string `schema:"bot_name"`
Token string `schema:"token"` Token string `schema:"token"`
TeamID string `schema:"team_id"` TeamID string `schema:"team_id"`
TeamDomain string `schema:"team_domain"` TeamDomain string `schema:"team_domain"`
@ -36,6 +38,8 @@ type IMessage struct {
UserID string `schema:"user_id"` UserID string `schema:"user_id"`
UserName string `schema:"user_name"` UserName string `schema:"user_name"`
PostId string `schema:"post_id"` PostId string `schema:"post_id"`
RawText string `schema:"raw_text"`
ServiceId string `schema:"service_id"`
Text string `schema:"text"` Text string `schema:"text"`
TriggerWord string `schema:"trigger_word"` TriggerWord string `schema:"trigger_word"`
} }

23
vendor/github.com/nlopes/slack/LICENSE generated vendored Normal file
View File

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

190
vendor/github.com/nlopes/slack/admin.go generated vendored Normal file
View File

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

76
vendor/github.com/nlopes/slack/attachments.go generated vendored Normal file
View File

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

57
vendor/github.com/nlopes/slack/backoff.go generated vendored Normal file
View File

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

261
vendor/github.com/nlopes/slack/channels.go generated vendored Normal file
View File

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

166
vendor/github.com/nlopes/slack/chat.go generated vendored Normal file
View File

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

10
vendor/github.com/nlopes/slack/comment.go generated vendored Normal file
View File

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

38
vendor/github.com/nlopes/slack/conversation.go generated vendored Normal file
View File

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

123
vendor/github.com/nlopes/slack/dnd.go generated vendored Normal file
View File

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

27
vendor/github.com/nlopes/slack/emoji.go generated vendored Normal file
View File

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

View File

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

30
vendor/github.com/nlopes/slack/examples/files/files.go generated vendored Normal file
View File

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

View File

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

View File

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

123
vendor/github.com/nlopes/slack/examples/pins/pins.go generated vendored Normal file
View File

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

View File

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

46
vendor/github.com/nlopes/slack/examples/stars/stars.go generated vendored Normal file
View File

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

17
vendor/github.com/nlopes/slack/examples/users/users.go generated vendored Normal file
View File

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

View File

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

274
vendor/github.com/nlopes/slack/files.go generated vendored Normal file
View File

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

293
vendor/github.com/nlopes/slack/groups.go generated vendored Normal file
View File

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

36
vendor/github.com/nlopes/slack/history.go generated vendored Normal file
View File

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

130
vendor/github.com/nlopes/slack/im.go generated vendored Normal file
View File

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

206
vendor/github.com/nlopes/slack/info.go generated vendored Normal file
View File

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

75
vendor/github.com/nlopes/slack/item.go generated vendored Normal file
View File

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

30
vendor/github.com/nlopes/slack/messageID.go generated vendored Normal file
View File

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

131
vendor/github.com/nlopes/slack/messages.go generated vendored Normal file
View File

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

119
vendor/github.com/nlopes/slack/misc.go generated vendored Normal file
View File

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

54
vendor/github.com/nlopes/slack/oauth.go generated vendored Normal file
View File

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

20
vendor/github.com/nlopes/slack/pagination.go generated vendored Normal file
View File

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

79
vendor/github.com/nlopes/slack/pins.go generated vendored Normal file
View File

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

246
vendor/github.com/nlopes/slack/reactions.go generated vendored Normal file
View File

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

39
vendor/github.com/nlopes/slack/rtm.go generated vendored Normal file
View File

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

137
vendor/github.com/nlopes/slack/search.go generated vendored Normal file
View File

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

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

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

135
vendor/github.com/nlopes/slack/stars.go generated vendored Normal file
View File

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

46
vendor/github.com/nlopes/slack/team.go generated vendored Normal file
View File

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

140
vendor/github.com/nlopes/slack/users.go generated vendored Normal file
View File

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

93
vendor/github.com/nlopes/slack/websocket.go generated vendored Normal file
View File

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

72
vendor/github.com/nlopes/slack/websocket_channels.go generated vendored Normal file
View File

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

23
vendor/github.com/nlopes/slack/websocket_dm.go generated vendored Normal file
View File

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

8
vendor/github.com/nlopes/slack/websocket_dnd.go generated vendored Normal file
View File

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

49
vendor/github.com/nlopes/slack/websocket_files.go generated vendored Normal file
View File

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

49
vendor/github.com/nlopes/slack/websocket_groups.go generated vendored Normal file
View File

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

92
vendor/github.com/nlopes/slack/websocket_internals.go generated vendored Normal file
View File

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

View File

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

117
vendor/github.com/nlopes/slack/websocket_misc.go generated vendored Normal file
View File

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

16
vendor/github.com/nlopes/slack/websocket_pins.go generated vendored Normal file
View File

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

83
vendor/github.com/nlopes/slack/websocket_proxy.go generated vendored Normal file
View File

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

25
vendor/github.com/nlopes/slack/websocket_reactions.go generated vendored Normal file
View File

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

14
vendor/github.com/nlopes/slack/websocket_stars.go generated vendored Normal file
View File

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

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

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

20
vendor/github.com/nlopes/slack/websocket_utils.go generated vendored Normal file
View File

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

27
vendor/golang.org/x/net/websocket/LICENSE generated vendored Normal file
View File

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

113
vendor/golang.org/x/net/websocket/client.go generated vendored Normal file
View File

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

583
vendor/golang.org/x/net/websocket/hybi.go generated vendored Normal file
View File

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

113
vendor/golang.org/x/net/websocket/server.go generated vendored Normal file
View File

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

411
vendor/golang.org/x/net/websocket/websocket.go generated vendored Normal file
View File

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

17
vendor/manifest vendored
View File

@ -110,6 +110,14 @@
"path": "/i18n", "path": "/i18n",
"notests": true "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", "importpath": "github.com/pborman/uuid",
"repository": "https://github.com/pborman/uuid", "repository": "https://github.com/pborman/uuid",
@ -186,6 +194,15 @@
"path": "/lex/httplex", "path": "/lex/httplex",
"notests": true "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", "importpath": "gopkg.in/gcfg.v1",
"repository": "https://gopkg.in/gcfg.v1", "repository": "https://gopkg.in/gcfg.v1",