diff --git a/.travis.yml b/.travis.yml index 2ed49274..a51918bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: go go: - #- 1.7.x - - 1.10.x - # - tip + - 1.11.x # we have everything vendored install: true diff --git a/bridge/discord/discord.go b/bridge/discord/discord.go index 9d7e2d69..116bf86d 100644 --- a/bridge/discord/discord.go +++ b/bridge/discord/discord.go @@ -423,7 +423,7 @@ func (b *Bdiscord) replaceUserMentions(text string) string { if err != nil { return m } - return member.User.Mention() + return strings.Replace(m, "@"+mention, member.User.Mention(), -1) }) b.Log.Debugf("Message with mention replaced: %s", text) return text diff --git a/bridge/slack/handlers.go b/bridge/slack/handlers.go new file mode 100644 index 00000000..56fd7026 --- /dev/null +++ b/bridge/slack/handlers.go @@ -0,0 +1,353 @@ +package bslack + +import ( + "bytes" + "fmt" + "html" + "regexp" + "time" + + "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/bridge/helper" + "github.com/nlopes/slack" +) + +func (b *Bslack) handleSlack() { + messages := make(chan *config.Message) + if b.GetString(incomingWebhookConfig) != "" { + b.Log.Debugf("Choosing webhooks based receiving") + go b.handleMatterHook(messages) + } else { + b.Log.Debugf("Choosing token based receiving") + go b.handleSlackClient(messages) + } + time.Sleep(time.Second) + b.Log.Debug("Start listening for Slack messages") + for message := range messages { + b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account) + + // cleanup the message + message.Text = b.replaceMention(message.Text) + message.Text = b.replaceVariable(message.Text) + message.Text = b.replaceChannel(message.Text) + message.Text = b.replaceURL(message.Text) + message.Text = html.UnescapeString(message.Text) + + // Add the avatar + message.Avatar = b.getAvatar(message.UserID) + + b.Log.Debugf("<= Message is %#v", message) + b.Remote <- *message + } +} + +func (b *Bslack) handleSlackClient(messages chan *config.Message) { + for msg := range b.rtm.IncomingEvents { + if msg.Type != sUserTyping && msg.Type != sLatencyReport { + b.Log.Debugf("== Receiving event %#v", msg.Data) + } + switch ev := msg.Data.(type) { + case *slack.MessageEvent: + if b.skipMessageEvent(ev) { + b.Log.Debugf("Skipped message: %#v", ev) + continue + } + rmsg, err := b.handleMessageEvent(ev) + if err != nil { + b.Log.Errorf("%#v", err) + continue + } + messages <- rmsg + case *slack.OutgoingErrorEvent: + b.Log.Debugf("%#v", ev.Error()) + case *slack.ChannelJoinedEvent: + var err error + b.users, err = b.sc.GetUsers() + if err != nil { + b.Log.Errorf("Could not reload users: %#v", err) + } + case *slack.ConnectedEvent: + var err error + b.channels, _, err = b.sc.GetConversations(&slack.GetConversationsParameters{ + Limit: 1000, + Types: []string{"public_channel,private_channel,mpim,im"}, + }) + if err != nil { + b.Log.Errorf("Channel list failed: %#v", err) + } + b.si = ev.Info + b.users, err = b.sc.GetUsers() + if err != nil { + b.Log.Errorf("Could not reload users: %#v", err) + } + case *slack.InvalidAuthEvent: + b.Log.Fatalf("Invalid Token %#v", ev) + case *slack.ConnectionErrorEvent: + b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) + default: + } + } +} + +func (b *Bslack) handleMatterHook(messages chan *config.Message) { + for { + message := b.mh.Receive() + b.Log.Debugf("receiving from matterhook (slack) %#v", message) + if message.UserName == "slackbot" { + continue + } + messages <- &config.Message{ + Username: message.UserName, + Text: message.Text, + Channel: message.ChannelName, + } + } +} + +var commentRE = regexp.MustCompile(`.*?commented: (.*)`) + +// handleDownloadFile handles file download +func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File) error { + // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra + // limit to 1MB for now + comment := "" + results := commentRE.FindAllStringSubmatch(rmsg.Text, -1) + if len(results) > 0 { + comment = results[0][1] + } + err := helper.HandleDownloadSize(b.Log, rmsg, file.Name, int64(file.Size), b.General) + if err != nil { + return err + } + // actually download the file + data, err := helper.DownloadFileAuth(file.URLPrivateDownload, "Bearer "+b.GetString(tokenConfig)) + if err != nil { + return fmt.Errorf("download %s failed %#v", file.URLPrivateDownload, err) + } + // add the downloaded data to the message + helper.HandleDownloadData(b.Log, rmsg, file.Name, comment, file.URLPrivateDownload, data, b.General) + return nil +} + +// handleUploadFile handles native upload of files +func (b *Bslack) handleUploadFile(msg *config.Message, channelID string) { + for _, f := range msg.Extra["file"] { + fi := f.(config.FileInfo) + if msg.Text == fi.Comment { + msg.Text = "" + } + /* because the result of the UploadFile is slower than the MessageEvent from slack + we can't match on the file ID yet, so we have to match on the filename too + */ + b.Log.Debugf("Adding file %s to cache %s", fi.Name, time.Now().String()) + b.cache.Add("filename"+fi.Name, time.Now()) + res, err := b.sc.UploadFile(slack.FileUploadParameters{ + Reader: bytes.NewReader(*fi.Data), + Filename: fi.Name, + Channels: []string{channelID}, + InitialComment: fi.Comment, + }) + if res.ID != "" { + b.Log.Debugf("Adding fileid %s to cache %s", res.ID, time.Now().String()) + b.cache.Add("file"+res.ID, time.Now()) + } + if err != nil { + b.Log.Errorf("uploadfile %#v", err) + } + } +} + +// handleMessageEvent handles the message events +func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, error) { + var err error + + // update the userlist on a channel_join + if ev.SubType == sChannelJoin { + if b.users, err = b.sc.GetUsers(); err != nil { + return nil, err + } + } + + // Edit message + if !b.GetBool(editDisableConfig) && ev.SubMessage != nil && ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp { + b.Log.Debugf("SubMessage %#v", ev.SubMessage) + ev.User = ev.SubMessage.User + ev.Text = ev.SubMessage.Text + b.GetString(editSuffixConfig) + } + + // use our own func because rtm.GetChannelInfo doesn't work for private channels + channelInfo, err := b.getChannelByID(ev.Channel) + if err != nil { + return nil, err + } + + rmsg := config.Message{ + Text: ev.Text, + Channel: channelInfo.Name, + Account: b.Account, + ID: "slack " + ev.Timestamp, + Extra: map[string][]interface{}{}, + } + + if b.useChannelID { + rmsg.Channel = "ID:" + channelInfo.ID + } + + // find the user id and name + if ev.User != "" && ev.SubType != sMessageDeleted && ev.SubType != sFileComment { + user, err := b.rtm.GetUserInfo(ev.User) + if err != nil { + return nil, err + } + rmsg.UserID = user.ID + rmsg.Username = user.Name + if user.Profile.DisplayName != "" { + rmsg.Username = user.Profile.DisplayName + } + } + + // See if we have some text in the attachments + if rmsg.Text == "" { + for _, attach := range ev.Attachments { + if attach.Text != "" { + if attach.Title != "" { + rmsg.Text = attach.Title + "\n" + } + rmsg.Text += attach.Text + } else { + rmsg.Text = attach.Fallback + } + } + } + + // when using webhookURL we can't check if it's our webhook or not for now + if rmsg.Username == "" && ev.BotID != "" && b.GetString(outgoingWebhookConfig) == "" { + bot, err := b.rtm.GetBotInfo(ev.BotID) + if err != nil { + return nil, err + } + if bot.Name != "" { + rmsg.Username = bot.Name + if ev.Username != "" { + rmsg.Username = ev.Username + } + rmsg.UserID = bot.ID + } + + // fixes issues with matterircd users + if bot.Name == "Slack API Tester" { + user, err := b.rtm.GetUserInfo(ev.User) + if err != nil { + return nil, err + } + rmsg.UserID = user.ID + rmsg.Username = user.Name + if user.Profile.DisplayName != "" { + rmsg.Username = user.Profile.DisplayName + } + } + } + + // file comments are set by the system (because there is no username given) + if ev.SubType == sFileComment { + rmsg.Username = sSystemUser + } + + // do we have a /me action + if ev.SubType == sMeMessage { + rmsg.Event = config.EVENT_USER_ACTION + } + + // Handle join/leave + if ev.SubType == sChannelLeave || ev.SubType == sChannelJoin { + rmsg.Username = sSystemUser + rmsg.Event = config.EVENT_JOIN_LEAVE + } + + // edited messages have a submessage, use this timestamp + if ev.SubMessage != nil { + rmsg.ID = "slack " + ev.SubMessage.Timestamp + } + + // deleted message event + if ev.SubType == sMessageDeleted { + rmsg.Text = config.EVENT_MSG_DELETE + rmsg.Event = config.EVENT_MSG_DELETE + rmsg.ID = "slack " + ev.DeletedTimestamp + } + + // topic change event + if ev.SubType == sChannelTopic || ev.SubType == sChannelPurpose { + rmsg.Event = config.EVENT_TOPIC_CHANGE + } + + // Only deleted messages can have a empty username and text + if (rmsg.Text == "" || rmsg.Username == "") && ev.SubType != sMessageDeleted && len(ev.Files) == 0 { + // this is probably a webhook we couldn't resolve + if ev.BotID != "" { + return nil, fmt.Errorf("probably an incoming webhook we couldn't resolve (maybe ourselves)") + } + return nil, fmt.Errorf("empty message and not a deleted message") + } + + // save the attachments, so that we can send them to other slack (compatible) bridges + if len(ev.Attachments) > 0 { + rmsg.Extra[sSlackAttachment] = append(rmsg.Extra[sSlackAttachment], ev.Attachments) + } + + // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra + for _, f := range ev.Files { + err := b.handleDownloadFile(&rmsg, &f) + if err != nil { + b.Log.Errorf("download failed: %s", err) + } + } + + return &rmsg, nil +} + +// skipMessageEvent skips event that need to be skipped :-) +func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { + if ev.SubType == sChannelLeave || ev.SubType == sChannelJoin { + return b.GetBool(noSendJoinConfig) + } + + // ignore pinned items + if ev.SubType == sPinnedItem || ev.SubType == sUnpinnedItem { + return true + } + + // do not send messages from ourself + if b.GetString(outgoingWebhookConfig) == "" && b.GetString(incomingWebhookConfig) == "" && ev.Username == b.si.User.Name { + return true + } + + // skip messages we made ourselves + if len(ev.Attachments) > 0 { + if ev.Attachments[0].CallbackID == "matterbridge_"+b.uuid { + return true + } + } + + if !b.GetBool(editDisableConfig) && ev.SubMessage != nil && ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp { + // it seems ev.SubMessage.Edited == nil when slack unfurls + // do not forward these messages #266 + if ev.SubMessage.Edited == nil { + return true + } + } + + for _, f := range ev.Files { + // if the file is in the cache and isn't older then a minute, skip it + if ts, ok := b.cache.Get("file" + f.ID); ok && time.Since(ts.(time.Time)) < time.Minute { + b.Log.Debugf("Not downloading file id %s which we uploaded", f.ID) + return true + } else if ts, ok := b.cache.Get("filename" + f.Name); ok && time.Since(ts.(time.Time)) < 10*time.Second { + b.Log.Debugf("Not downloading file name %s which we uploaded", f.Name) + return true + } + b.Log.Debugf("Not skipping %s %s", f.Name, time.Now().String()) + } + + return false +} diff --git a/bridge/slack/helpers.go b/bridge/slack/helpers.go new file mode 100644 index 00000000..9af5dfac --- /dev/null +++ b/bridge/slack/helpers.go @@ -0,0 +1,113 @@ +package bslack + +import ( + "fmt" + "regexp" + "strings" + + "github.com/nlopes/slack" +) + +func (b *Bslack) getUsername(id string) string { + for _, u := range b.users { + if u.ID == id { + if u.Profile.DisplayName != "" { + return u.Profile.DisplayName + } + return u.Name + } + } + b.Log.Warnf("Could not find user with ID '%s'", id) + return "" +} + +func (b *Bslack) getAvatar(userid string) string { + for _, u := range b.users { + if userid == u.ID { + return u.Profile.Image48 + } + } + return "" +} + +func (b *Bslack) getChannel(channel string) (*slack.Channel, error) { + if strings.HasPrefix(channel, "ID:") { + return b.getChannelByID(strings.TrimPrefix(channel, "ID:")) + } + return b.getChannelByName(channel) +} + +func (b *Bslack) getChannelByName(name string) (*slack.Channel, error) { + if b.channels == nil { + return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.Account, name) + } + for _, channel := range b.channels { + if channel.Name == name { + return &channel, nil + } + } + return nil, fmt.Errorf("%s: channel %s not found", b.Account, name) +} + +func (b *Bslack) getChannelByID(ID string) (*slack.Channel, error) { + if b.channels == nil { + return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.Account, ID) + } + for _, channel := range b.channels { + if channel.ID == ID { + return &channel, nil + } + } + return nil, fmt.Errorf("%s: channel %s not found", b.Account, ID) +} + +var ( + mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`) + channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`) + variableRE = regexp.MustCompile(``) + urlRE = regexp.MustCompile(`<(.*?)(\|.*?)?>`) +) + +// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users +func (b *Bslack) replaceMention(text string) string { + replaceFunc := func(match string) string { + userID := strings.Trim(match, "@<>") + if username := b.getUsername(userID); userID != "" { + return "@" + username + } + return match + } + return mentionRE.ReplaceAllStringFunc(text, replaceFunc) +} + +// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users +func (b *Bslack) replaceChannel(text string) string { + for _, r := range channelRE.FindAllStringSubmatch(text, -1) { + text = strings.Replace(text, r[0], "#"+r[1], 1) + } + return text +} + +// @see https://api.slack.com/docs/message-formatting#variables +func (b *Bslack) replaceVariable(text string) string { + for _, r := range variableRE.FindAllStringSubmatch(text, -1) { + if r[2] != "" { + text = strings.Replace(text, r[0], "@"+r[2], 1) + } else { + text = strings.Replace(text, r[0], "@"+r[1], 1) + } + } + return text +} + +// @see https://api.slack.com/docs/message-formatting#linking_to_urls +func (b *Bslack) replaceURL(text string) string { + for _, r := range urlRE.FindAllStringSubmatch(text, -1) { + if len(strings.TrimSpace(r[2])) == 1 { // A display text separator was found, but the text was blank + text = strings.Replace(text, r[0], "", 1) + } else { + text = strings.Replace(text, r[0], r[1], 1) + } + } + return text +} diff --git a/bridge/slack/slack.go b/bridge/slack/slack.go index e6f69ed8..6524f974 100644 --- a/bridge/slack/slack.go +++ b/bridge/slack/slack.go @@ -1,14 +1,10 @@ package bslack import ( - "bytes" "errors" "fmt" - "html" - "regexp" "strings" "sync" - "time" "github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge/config" @@ -23,22 +19,52 @@ type Bslack struct { mh *matterhook.Client sc *slack.Client rtm *slack.RTM - Users []slack.User - Usergroups []slack.UserGroup + users []slack.User si *slack.Info channels []slack.Channel cache *lru.Cache - UseChannelID bool + useChannelID bool uuid string *bridge.Config sync.RWMutex } -const messageDeleted = "message_deleted" +const ( + sChannelJoin = "channel_join" + sChannelLeave = "channel_leave" + sMessageDeleted = "message_deleted" + sSlackAttachment = "slack_attachment" + sPinnedItem = "pinned_item" + sUnpinnedItem = "unpinned_item" + sChannelTopic = "channel_topic" + sChannelPurpose = "channel_purpose" + sFileComment = "file_comment" + sMeMessage = "me_message" + sUserTyping = "user_typing" + sLatencyReport = "latency_report" + sSystemUser = "system" + + tokenConfig = "Token" + incomingWebhookConfig = "WebhookBindAddress" + outgoingWebhookConfig = "WebhookURL" + skipTLSConfig = "SkipTLSVerify" + useNickPrefixConfig = "PrefixMessagesWithNick" + editDisableConfig = "EditDisable" + editSuffixConfig = "EditSuffix" + iconURLConfig = "iconurl" + noSendJoinConfig = "nosendjoinpart" +) func New(cfg *bridge.Config) bridge.Bridger { - b := &Bslack{Config: cfg, uuid: xid.New().String()} - b.cache, _ = lru.New(5000) + newCache, err := lru.New(5000) + if err != nil { + cfg.Log.Fatalf("Could not create LRU cache for Slack bridge: %v", err) + } + b := &Bslack{ + Config: cfg, + uuid: xid.New().String(), + cache: newCache, + } return b } @@ -49,50 +75,54 @@ func (b *Bslack) Command(cmd string) string { func (b *Bslack) Connect() error { b.RLock() defer b.RUnlock() - if b.GetString("WebhookBindAddress") != "" { - if b.GetString("WebhookURL") != "" { + if b.GetString(incomingWebhookConfig) != "" { + if b.GetString(outgoingWebhookConfig) != "" { b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)") - b.mh = matterhook.New(b.GetString("WebhookURL"), - matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), - BindAddress: b.GetString("WebhookBindAddress")}) - } else if b.GetString("Token") != "" { + b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{ + InsecureSkipVerify: b.GetBool(skipTLSConfig), + BindAddress: b.GetString(incomingWebhookConfig), + }) + } else if b.GetString(tokenConfig) != "" { b.Log.Info("Connecting using token (sending)") - b.sc = slack.New(b.GetString("Token")) + b.sc = slack.New(b.GetString(tokenConfig)) b.rtm = b.sc.NewRTM() go b.rtm.ManageConnection() b.Log.Info("Connecting using webhookbindaddress (receiving)") - b.mh = matterhook.New(b.GetString("WebhookURL"), - matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), - BindAddress: b.GetString("WebhookBindAddress")}) + b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{ + InsecureSkipVerify: b.GetBool(skipTLSConfig), + BindAddress: b.GetString(incomingWebhookConfig), + }) } else { b.Log.Info("Connecting using webhookbindaddress (receiving)") - b.mh = matterhook.New(b.GetString("WebhookURL"), - matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), - BindAddress: b.GetString("WebhookBindAddress")}) + b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{ + InsecureSkipVerify: b.GetBool(skipTLSConfig), + BindAddress: b.GetString(incomingWebhookConfig), + }) } go b.handleSlack() return nil } - if b.GetString("WebhookURL") != "" { + if b.GetString(outgoingWebhookConfig) != "" { b.Log.Info("Connecting using webhookurl (sending)") - b.mh = matterhook.New(b.GetString("WebhookURL"), - matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), - DisableServer: true}) - if b.GetString("Token") != "" { + b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{ + InsecureSkipVerify: b.GetBool(skipTLSConfig), + DisableServer: true, + }) + if b.GetString(tokenConfig) != "" { b.Log.Info("Connecting using token (receiving)") - b.sc = slack.New(b.GetString("Token")) + b.sc = slack.New(b.GetString(tokenConfig)) b.rtm = b.sc.NewRTM() go b.rtm.ManageConnection() go b.handleSlack() } - } else if b.GetString("Token") != "" { + } else if b.GetString(tokenConfig) != "" { b.Log.Info("Connecting using token (sending and receiving)") - b.sc = slack.New(b.GetString("Token")) + b.sc = slack.New(b.GetString(tokenConfig)) b.rtm = b.sc.NewRTM() go b.rtm.ManageConnection() go b.handleSlack() } - if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" && b.GetString("Token") == "" { + if b.GetString(incomingWebhookConfig) == "" && b.GetString(outgoingWebhookConfig) == "" && b.GetString(tokenConfig) == "" { return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Token configured") } return nil @@ -106,7 +136,7 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error { // use ID:channelid and resolve it to the actual name idcheck := strings.Split(channel.Name, "ID:") if len(idcheck) > 1 { - b.UseChannelID = true + b.useChannelID = true ch, err := b.sc.GetChannelInfo(idcheck[1]) if err != nil { return err @@ -119,7 +149,7 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error { // we can only join channels using the API if b.sc != nil { - if strings.HasPrefix(b.GetString("Token"), "xoxb") { + if strings.HasPrefix(b.GetString(tokenConfig), "xoxb") { // TODO check if bot has already joined channel return nil } @@ -146,11 +176,14 @@ func (b *Bslack) Send(msg config.Message) (string, error) { } // Use webhook to send the message - if b.GetString("WebhookURL") != "" { + if b.GetString(outgoingWebhookConfig) != "" { return b.sendWebhook(msg) } - channelID := b.getChannelID(msg.Channel) + channelInfo, err := b.getChannel(msg.Channel) + if err != nil { + return "", fmt.Errorf("could not send message: %v", err) + } // Delete message if msg.Event == config.EVENT_MSG_DELETE { @@ -160,7 +193,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) { } // we get a "slack ", split it ts := strings.Fields(msg.ID) - _, _, err := b.sc.DeleteMessage(channelID, ts[1]) + _, _, err = b.sc.DeleteMessage(channelInfo.ID, ts[1]) if err != nil { return msg.ID, err } @@ -168,14 +201,14 @@ func (b *Bslack) Send(msg config.Message) (string, error) { } // Prepend nick if configured - if b.GetBool("PrefixMessagesWithNick") { + if b.GetBool(useNickPrefixConfig) { msg.Text = msg.Username + msg.Text } // Edit message if we have an ID if msg.ID != "" { ts := strings.Fields(msg.ID) - _, _, _, err := b.sc.UpdateMessage(channelID, ts[1], msg.Text) + _, _, _, err = b.sc.UpdateMessage(channelInfo.ID, ts[1], msg.Text) if err != nil { return msg.ID, err } @@ -184,12 +217,12 @@ func (b *Bslack) Send(msg config.Message) (string, error) { // create slack new post parameters np := slack.NewPostMessageParameters() - if b.GetBool("PrefixMessagesWithNick") { + if b.GetBool(useNickPrefixConfig) { np.AsUser = true } np.Username = msg.Username np.LinkNames = 1 // replace mentions - np.IconURL = config.GetIconURL(&msg, b.GetString("iconurl")) + np.IconURL = config.GetIconURL(&msg, b.GetString(iconURLConfig)) if msg.Avatar != "" { np.IconURL = msg.Avatar } @@ -198,8 +231,8 @@ func (b *Bslack) Send(msg config.Message) (string, error) { // add file attachments np.Attachments = append(np.Attachments, b.createAttach(msg.Extra)...) // add slack attachments (from another slack bridge) - if len(msg.Extra["slack_attachment"]) > 0 { - for _, attach := range msg.Extra["slack_attachment"] { + if msg.Extra != nil { + for _, attach := range msg.Extra[sSlackAttachment] { np.Attachments = append(np.Attachments, attach.([]slack.Attachment)...) } } @@ -207,16 +240,17 @@ func (b *Bslack) Send(msg config.Message) (string, error) { // Upload a file if it exists if msg.Extra != nil { for _, rmsg := range helper.HandleExtra(&msg, b.General) { - b.sc.PostMessage(channelID, rmsg.Username+rmsg.Text, np) - } - // check if we have files to upload (from slack, telegram or mattermost) - if len(msg.Extra["file"]) > 0 { - b.handleUploadFile(&msg, channelID) + _, _, err = b.sc.PostMessage(channelInfo.ID, rmsg.Username+rmsg.Text, np) + if err != nil { + b.Log.Error(err) + } } + // Upload files if necessary (from Slack, Telegram or Mattermost). + b.handleUploadFile(&msg, channelInfo.ID) } // Post normal message - _, id, err := b.sc.PostMessage(channelID, msg.Text, np) + _, id, err := b.sc.PostMessage(channelInfo.ID, msg.Text, np) if err != nil { return "", err } @@ -227,405 +261,37 @@ func (b *Bslack) Reload(cfg *bridge.Config) (string, error) { return "", nil } -func (b *Bslack) getAvatar(userid string) string { - var avatar string - if b.Users != nil { - for _, u := range b.Users { - if userid == u.ID { - return u.Profile.Image48 - } - } - } - return avatar -} - -/* -func (b *Bslack) getChannelByName(name string) (*slack.Channel, error) { - if b.channels == nil { - return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.Account, name) - } - for _, channel := range b.channels { - if channel.Name == name { - return &channel, nil - } - } - return nil, fmt.Errorf("%s: channel %s not found", b.Account, name) -} -*/ - -func (b *Bslack) getChannelByID(ID string) (*slack.Channel, error) { - if b.channels == nil { - return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.Account, ID) - } - for _, channel := range b.channels { - if channel.ID == ID { - return &channel, nil - } - } - return nil, fmt.Errorf("%s: channel %s not found", b.Account, ID) -} - -func (b *Bslack) handleSlack() { - messages := make(chan *config.Message) - if b.GetString("WebhookBindAddress") != "" { - b.Log.Debugf("Choosing webhooks based receiving") - go b.handleMatterHook(messages) - } else { - b.Log.Debugf("Choosing token based receiving") - go b.handleSlackClient(messages) - } - time.Sleep(time.Second) - b.Log.Debug("Start listening for Slack messages") - for message := range messages { - b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account) - - // cleanup the message - message.Text = b.replaceMention(message.Text) - message.Text = b.replaceVariable(message.Text) - message.Text = b.replaceChannel(message.Text) - message.Text = b.replaceURL(message.Text) - message.Text = html.UnescapeString(message.Text) - - // Add the avatar - message.Avatar = b.getAvatar(message.UserID) - - b.Log.Debugf("<= Message is %#v", message) - b.Remote <- *message - } -} - -func (b *Bslack) handleSlackClient(messages chan *config.Message) { - for msg := range b.rtm.IncomingEvents { - if msg.Type != "user_typing" && msg.Type != "latency_report" { - b.Log.Debugf("== Receiving event %#v", msg.Data) - } - switch ev := msg.Data.(type) { - case *slack.MessageEvent: - if b.skipMessageEvent(ev) { - b.Log.Debugf("Skipped message: %#v", ev) - continue - } - rmsg, err := b.handleMessageEvent(ev) - if err != nil { - b.Log.Errorf("%#v", err) - continue - } - messages <- rmsg - case *slack.OutgoingErrorEvent: - b.Log.Debugf("%#v", ev.Error()) - case *slack.ChannelJoinedEvent: - b.Users, _ = b.sc.GetUsers() - b.Usergroups, _ = b.sc.GetUserGroups() - case *slack.ConnectedEvent: - var err error - b.channels, _, err = b.sc.GetConversations(&slack.GetConversationsParameters{Limit: 1000, Types: []string{"public_channel,private_channel,mpim,im"}}) - if err != nil { - b.Log.Errorf("Channel list failed: %#v", err) - } - b.si = ev.Info - b.Users, _ = b.sc.GetUsers() - b.Usergroups, _ = b.sc.GetUserGroups() - case *slack.InvalidAuthEvent: - b.Log.Fatalf("Invalid Token %#v", ev) - case *slack.ConnectionErrorEvent: - b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) - default: - } - } -} - -func (b *Bslack) handleMatterHook(messages chan *config.Message) { - for { - message := b.mh.Receive() - b.Log.Debugf("receiving from matterhook (slack) %#v", message) - if message.UserName == "slackbot" { - continue - } - messages <- &config.Message{Username: message.UserName, Text: message.Text, Channel: message.ChannelName} - } -} - -func (b *Bslack) userName(id string) string { - for _, u := range b.Users { - if u.ID == id { - if u.Profile.DisplayName != "" { - return u.Profile.DisplayName - } - return u.Name - } - } - return "" -} - -/* -func (b *Bslack) userGroupName(id string) string { - for _, u := range b.Usergroups { - if u.ID == id { - return u.Name - } - } - return "" -} -*/ - -// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users -func (b *Bslack) replaceMention(text string) string { - results := regexp.MustCompile(`<@([a-zA-Z0-9]+)>`).FindAllStringSubmatch(text, -1) - for _, r := range results { - text = strings.Replace(text, "<@"+r[1]+">", "@"+b.userName(r[1]), -1) - } - return text -} - -// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users -func (b *Bslack) replaceChannel(text string) string { - results := regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`).FindAllStringSubmatch(text, -1) - for _, r := range results { - text = strings.Replace(text, r[0], "#"+r[1], -1) - } - return text -} - -// @see https://api.slack.com/docs/message-formatting#variables -func (b *Bslack) replaceVariable(text string) string { - results := regexp.MustCompile(``).FindAllStringSubmatch(text, -1) - for _, r := range results { - if r[2] != "" { - text = strings.Replace(text, r[0], "@"+r[2], -1) - } else { - text = strings.Replace(text, r[0], "@"+r[1], -1) - } - } - return text -} - -// @see https://api.slack.com/docs/message-formatting#linking_to_urls -func (b *Bslack) replaceURL(text string) string { - results := regexp.MustCompile(`<(.*?)(\|.*?)?>`).FindAllStringSubmatch(text, -1) - for _, r := range results { - if len(strings.TrimSpace(r[2])) == 1 { // A display text separator was found, but the text was blank - text = strings.Replace(text, r[0], "", -1) - } else { - text = strings.Replace(text, r[0], r[1], -1) - } - } - return text -} - func (b *Bslack) createAttach(extra map[string][]interface{}) []slack.Attachment { - var attachs []slack.Attachment + var attachements []slack.Attachment for _, v := range extra["attachments"] { entry := v.(map[string]interface{}) - s := slack.Attachment{} - s.Fallback = entry["fallback"].(string) - s.Color = entry["color"].(string) - s.Pretext = entry["pretext"].(string) - s.AuthorName = entry["author_name"].(string) - s.AuthorLink = entry["author_link"].(string) - s.AuthorIcon = entry["author_icon"].(string) - s.Title = entry["title"].(string) - s.TitleLink = entry["title_link"].(string) - s.Text = entry["text"].(string) - s.ImageURL = entry["image_url"].(string) - s.ThumbURL = entry["thumb_url"].(string) - s.Footer = entry["footer"].(string) - s.FooterIcon = entry["footer_icon"].(string) - attachs = append(attachs, s) + s := slack.Attachment{ + Fallback: extractStringField(entry, "fallback"), + Color: extractStringField(entry, "color"), + Pretext: extractStringField(entry, "pretext"), + AuthorName: extractStringField(entry, "author_name"), + AuthorLink: extractStringField(entry, "author_link"), + AuthorIcon: extractStringField(entry, "author_icon"), + Title: extractStringField(entry, "title"), + TitleLink: extractStringField(entry, "title_link"), + Text: extractStringField(entry, "text"), + ImageURL: extractStringField(entry, "image_url"), + ThumbURL: extractStringField(entry, "thumb_url"), + Footer: extractStringField(entry, "footer"), + FooterIcon: extractStringField(entry, "footer_icon"), + } + attachements = append(attachements, s) } - return attachs + return attachements } -// handleDownloadFile handles file download -func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File) error { - // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra - // limit to 1MB for now - comment := "" - results := regexp.MustCompile(`.*?commented: (.*)`).FindAllStringSubmatch(rmsg.Text, -1) - if len(results) > 0 { - comment = results[0][1] - } - err := helper.HandleDownloadSize(b.Log, rmsg, file.Name, int64(file.Size), b.General) - if err != nil { - return err - } - // actually download the file - data, err := helper.DownloadFileAuth(file.URLPrivateDownload, "Bearer "+b.GetString("Token")) - if err != nil { - return fmt.Errorf("download %s failed %#v", file.URLPrivateDownload, err) - } - // add the downloaded data to the message - helper.HandleDownloadData(b.Log, rmsg, file.Name, comment, file.URLPrivateDownload, data, b.General) - return nil -} - -// handleUploadFile handles native upload of files -func (b *Bslack) handleUploadFile(msg *config.Message, channelID string) (string, error) { - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - if msg.Text == fi.Comment { - msg.Text = "" - } - /* because the result of the UploadFile is slower than the MessageEvent from slack - we can't match on the file ID yet, so we have to match on the filename too - */ - b.Log.Debugf("Adding file %s to cache %s", fi.Name, time.Now().String()) - b.cache.Add("filename"+fi.Name, time.Now()) - res, err := b.sc.UploadFile(slack.FileUploadParameters{ - Reader: bytes.NewReader(*fi.Data), - Filename: fi.Name, - Channels: []string{channelID}, - InitialComment: fi.Comment, - }) - if res.ID != "" { - b.Log.Debugf("Adding fileid %s to cache %s", res.ID, time.Now().String()) - b.cache.Add("file"+res.ID, time.Now()) - } - if err != nil { - b.Log.Errorf("uploadfile %#v", err) +func extractStringField(data map[string]interface{}, field string) string { + if rawValue, found := data[field]; found { + if value, ok := rawValue.(string); ok { + return value } } - return "", nil -} - -// handleMessageEvent handles the message events -func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, error) { - // update the userlist on a channel_join - if ev.SubType == "channel_join" { - b.Users, _ = b.sc.GetUsers() - } - - // Edit message - if !b.GetBool("EditDisable") && ev.SubMessage != nil && ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp { - b.Log.Debugf("SubMessage %#v", ev.SubMessage) - ev.User = ev.SubMessage.User - ev.Text = ev.SubMessage.Text + b.GetString("EditSuffix") - } - - // use our own func because rtm.GetChannelInfo doesn't work for private channels - channel, err := b.getChannelByID(ev.Channel) - if err != nil { - return nil, err - } - - rmsg := config.Message{Text: ev.Text, Channel: channel.Name, Account: b.Account, ID: "slack " + ev.Timestamp, Extra: make(map[string][]interface{})} - - if b.UseChannelID { - rmsg.Channel = "ID:" + channel.ID - } - - // find the user id and name - if ev.User != "" && ev.SubType != messageDeleted && ev.SubType != "file_comment" { - user, err := b.rtm.GetUserInfo(ev.User) - if err != nil { - return nil, err - } - rmsg.UserID = user.ID - rmsg.Username = user.Name - if user.Profile.DisplayName != "" { - rmsg.Username = user.Profile.DisplayName - } - } - - // See if we have some text in the attachments - if rmsg.Text == "" { - for _, attach := range ev.Attachments { - if attach.Text != "" { - if attach.Title != "" { - rmsg.Text = attach.Title + "\n" - } - rmsg.Text += attach.Text - } else { - rmsg.Text = attach.Fallback - } - } - } - - // when using webhookURL we can't check if it's our webhook or not for now - if rmsg.Username == "" && ev.BotID != "" && b.GetString("WebhookURL") == "" { - bot, err := b.rtm.GetBotInfo(ev.BotID) - if err != nil { - return nil, err - } - if bot.Name != "" { - rmsg.Username = bot.Name - if ev.Username != "" { - rmsg.Username = ev.Username - } - rmsg.UserID = bot.ID - } - - // fixes issues with matterircd users - if bot.Name == "Slack API Tester" { - user, err := b.rtm.GetUserInfo(ev.User) - if err != nil { - return nil, err - } - rmsg.UserID = user.ID - rmsg.Username = user.Name - if user.Profile.DisplayName != "" { - rmsg.Username = user.Profile.DisplayName - } - } - } - - // file comments are set by the system (because there is no username given) - if ev.SubType == "file_comment" { - rmsg.Username = "system" - } - - // do we have a /me action - if ev.SubType == "me_message" { - rmsg.Event = config.EVENT_USER_ACTION - } - - // Handle join/leave - if ev.SubType == "channel_leave" || ev.SubType == "channel_join" { - rmsg.Username = "system" - rmsg.Event = config.EVENT_JOIN_LEAVE - } - - // edited messages have a submessage, use this timestamp - if ev.SubMessage != nil { - rmsg.ID = "slack " + ev.SubMessage.Timestamp - } - - // deleted message event - if ev.SubType == messageDeleted { - rmsg.Text = config.EVENT_MSG_DELETE - rmsg.Event = config.EVENT_MSG_DELETE - rmsg.ID = "slack " + ev.DeletedTimestamp - } - - // topic change event - if ev.SubType == "channel_topic" || ev.SubType == "channel_purpose" { - rmsg.Event = config.EVENT_TOPIC_CHANGE - } - - // Only deleted messages can have a empty username and text - if (rmsg.Text == "" || rmsg.Username == "") && ev.SubType != messageDeleted && len(ev.Files) == 0 { - // this is probably a webhook we couldn't resolve - if ev.BotID != "" { - return nil, fmt.Errorf("probably an incoming webhook we couldn't resolve (maybe ourselves)") - } - return nil, fmt.Errorf("empty message and not a deleted message") - } - - // save the attachments, so that we can send them to other slack (compatible) bridges - if len(ev.Attachments) > 0 { - rmsg.Extra["slack_attachment"] = append(rmsg.Extra["slack_attachment"], ev.Attachments) - } - - // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra - if len(ev.Files) > 0 { - for _, f := range ev.Files { - err := b.handleDownloadFile(&rmsg, &f) - if err != nil { - b.Log.Errorf("download failed: %s", err) - } - } - } - - return &rmsg, nil + return "" } // sendWebhook uses the configured WebhookURL to send the message @@ -635,39 +301,48 @@ func (b *Bslack) sendWebhook(msg config.Message) (string, error) { return "", nil } - if b.GetBool("PrefixMessagesWithNick") { + if b.GetBool(useNickPrefixConfig) { msg.Text = msg.Username + msg.Text } if msg.Extra != nil { // this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE for _, rmsg := range helper.HandleExtra(&msg, b.General) { - iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl")) - matterMessage := matterhook.OMessage{IconURL: iconURL, Channel: msg.Channel, UserName: rmsg.Username, Text: rmsg.Text} - b.mh.Send(matterMessage) + iconURL := config.GetIconURL(&rmsg, b.GetString(iconURLConfig)) + matterMessage := matterhook.OMessage{ + IconURL: iconURL, + Channel: msg.Channel, + UserName: rmsg.Username, + Text: rmsg.Text, + } + if err := b.mh.Send(matterMessage); err != nil { + b.Log.Errorf("Failed to send message: %v", err) + } } // webhook doesn't support file uploads, so we add the url manually - if len(msg.Extra["file"]) > 0 { - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - if fi.URL != "" { - msg.Text += " " + fi.URL - } + for _, f := range msg.Extra["file"] { + fi := f.(config.FileInfo) + if fi.URL != "" { + msg.Text += " " + fi.URL } } } // if we have native slack_attachments add them var attachs []slack.Attachment - if len(msg.Extra["slack_attachment"]) > 0 { - for _, attach := range msg.Extra["slack_attachment"] { - attachs = append(attachs, attach.([]slack.Attachment)...) - } + for _, attach := range msg.Extra[sSlackAttachment] { + attachs = append(attachs, attach.([]slack.Attachment)...) } - iconURL := config.GetIconURL(&msg, b.GetString("iconurl")) - matterMessage := matterhook.OMessage{IconURL: iconURL, Attachments: attachs, Channel: msg.Channel, UserName: msg.Username, Text: msg.Text} + iconURL := config.GetIconURL(&msg, b.GetString(iconURLConfig)) + matterMessage := matterhook.OMessage{ + IconURL: iconURL, + Attachments: attachs, + Channel: msg.Channel, + UserName: msg.Username, + Text: msg.Text, + } if msg.Avatar != "" { matterMessage.IconURL = msg.Avatar } @@ -678,67 +353,3 @@ func (b *Bslack) sendWebhook(msg config.Message) (string, error) { } return "", nil } - -// skipMessageEvent skips event that need to be skipped :-) -func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { - if ev.SubType == "channel_leave" || ev.SubType == "channel_join" { - return b.GetBool("nosendjoinpart") - } - - // ignore pinned items - if ev.SubType == "pinned_item" || ev.SubType == "unpinned_item" { - return true - } - - // do not send messages from ourself - if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" && ev.Username == b.si.User.Name { - return true - } - - // skip messages we made ourselves - if len(ev.Attachments) > 0 { - if ev.Attachments[0].CallbackID == "matterbridge_"+b.uuid { - return true - } - } - - if !b.GetBool("EditDisable") && ev.SubMessage != nil && ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp { - // it seems ev.SubMessage.Edited == nil when slack unfurls - // do not forward these messages #266 - if ev.SubMessage.Edited == nil { - return true - } - } - - if len(ev.Files) > 0 { - for _, f := range ev.Files { - // if the file is in the cache and isn't older then a minute, skip it - if ts, ok := b.cache.Get("file" + f.ID); ok && time.Since(ts.(time.Time)) < time.Minute { - b.Log.Debugf("Not downloading file id %s which we uploaded", f.ID) - return true - } else { - if ts, ok := b.cache.Get("filename" + f.Name); ok && time.Since(ts.(time.Time)) < time.Second*10 { - b.Log.Debugf("Not downloading file name %s which we uploaded", f.Name) - return true - } else { - b.Log.Debugf("Not skipping %s %s", f.Name, time.Now().String()) - } - } - } - } - - return false -} - -func (b *Bslack) getChannelID(name string) string { - idcheck := strings.Split(name, "ID:") - if len(idcheck) > 1 { - return idcheck[1] - } - for _, channel := range b.channels { - if channel.Name == name { - return channel.ID - } - } - return "" -} diff --git a/ci/bintray.sh b/ci/bintray.sh index 1ca8ba25..b0fb7d6a 100755 --- a/ci/bintray.sh +++ b/ci/bintray.sh @@ -1,5 +1,5 @@ #!/bin/bash -go version |grep go1.10 || exit +go version | grep go1.11 || exit VERSION=$(git describe --tags) mkdir ci/binaries GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-windows-amd64.exe diff --git a/gateway/gateway.go b/gateway/gateway.go index 33c42795..57752718 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -591,6 +591,7 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1) nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1) + nick = strings.Replace(nick, "{GATEWAY}", gw.Name, -1) nick = strings.Replace(nick, "{LABEL}", br.GetString("Label"), -1) nick = strings.Replace(nick, "{NICK}", msg.Username, -1) nick = strings.Replace(nick, "{CHANNEL}", msg.Channel, -1) diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go index bf9461c7..0a1470ff 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -172,18 +172,27 @@ func TestNewRouter(t *testing.T) { assert.Equal(t, 3, len(r.Gateways["bridge2"].Bridges)) assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels)) assert.Equal(t, 3, len(r.Gateways["bridge2"].Channels)) - assert.Equal(t, &config.ChannelInfo{Name: "42wim/testroom", Direction: "out", - ID: "42wim/testroomgitter.42wim", Account: "gitter.42wim", - SameChannel: map[string]bool{"bridge2": false}}, - r.Gateways["bridge2"].Channels["42wim/testroomgitter.42wim"]) - assert.Equal(t, &config.ChannelInfo{Name: "42wim/testroom", Direction: "in", - ID: "42wim/testroomgitter.42wim", Account: "gitter.42wim", - SameChannel: map[string]bool{"bridge1": false}}, - r.Gateways["bridge1"].Channels["42wim/testroomgitter.42wim"]) - assert.Equal(t, &config.ChannelInfo{Name: "general", Direction: "inout", - ID: "generaldiscord.test", Account: "discord.test", - SameChannel: map[string]bool{"bridge1": false}}, - r.Gateways["bridge1"].Channels["generaldiscord.test"]) + assert.Equal(t, &config.ChannelInfo{ + Name: "42wim/testroom", + Direction: "out", + ID: "42wim/testroomgitter.42wim", + Account: "gitter.42wim", + SameChannel: map[string]bool{"bridge2": false}, + }, r.Gateways["bridge2"].Channels["42wim/testroomgitter.42wim"]) + assert.Equal(t, &config.ChannelInfo{ + Name: "42wim/testroom", + Direction: "in", + ID: "42wim/testroomgitter.42wim", + Account: "gitter.42wim", + SameChannel: map[string]bool{"bridge1": false}, + }, r.Gateways["bridge1"].Channels["42wim/testroomgitter.42wim"]) + assert.Equal(t, &config.ChannelInfo{ + Name: "general", + Direction: "inout", + ID: "generaldiscord.test", + Account: "discord.test", + SameChannel: map[string]bool{"bridge1": false}, + }, r.Gateways["bridge1"].Channels["generaldiscord.test"]) } func TestGetDestChannel(t *testing.T) { @@ -192,11 +201,23 @@ func TestGetDestChannel(t *testing.T) { for _, br := range r.Gateways["bridge1"].Bridges { switch br.Account { case "discord.test": - assert.Equal(t, []config.ChannelInfo{{Name: "general", Account: "discord.test", Direction: "inout", ID: "generaldiscord.test", SameChannel: map[string]bool{"bridge1": false}, Options: config.ChannelOptions{Key: ""}}}, - r.Gateways["bridge1"].getDestChannel(msg, *br)) + assert.Equal(t, []config.ChannelInfo{{ + Name: "general", + Account: "discord.test", + Direction: "inout", + ID: "generaldiscord.test", + SameChannel: map[string]bool{"bridge1": false}, + Options: config.ChannelOptions{Key: ""}, + }}, r.Gateways["bridge1"].getDestChannel(msg, *br)) case "slack.test": - assert.Equal(t, []config.ChannelInfo{{Name: "testing", Account: "slack.test", Direction: "out", ID: "testingslack.test", SameChannel: map[string]bool{"bridge1": false}, Options: config.ChannelOptions{Key: ""}}}, - r.Gateways["bridge1"].getDestChannel(msg, *br)) + assert.Equal(t, []config.ChannelInfo{{ + Name: "testing", + Account: "slack.test", + Direction: "out", + ID: "testingslack.test", + SameChannel: map[string]bool{"bridge1": false}, + Options: config.ChannelOptions{Key: ""}, + }}, r.Gateways["bridge1"].getDestChannel(msg, *br)) case "gitter.42wim": assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br)) case "irc.freenode": @@ -226,35 +247,87 @@ func TestGetDestChannelAdvanced(t *testing.T) { } switch gw.Name { case "bridge": - if (msg.Channel == "#main" || msg.Channel == "-1111111111111" || msg.Channel == "irc") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz" || msg.Account == "slack.zzz") { + if (msg.Channel == "#main" || msg.Channel == "-1111111111111" || msg.Channel == "irc") && + (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz" || msg.Account == "slack.zzz") { hits[gw.Name]++ switch br.Account { case "irc.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "#main", Account: "irc.zzz", Direction: "inout", ID: "#mainirc.zzz", SameChannel: map[string]bool{"bridge": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + assert.Equal(t, []config.ChannelInfo{{ + Name: "#main", + Account: "irc.zzz", + Direction: "inout", + ID: "#mainirc.zzz", + SameChannel: map[string]bool{"bridge": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) case "telegram.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "-1111111111111", Account: "telegram.zzz", Direction: "inout", ID: "-1111111111111telegram.zzz", SameChannel: map[string]bool{"bridge": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + assert.Equal(t, []config.ChannelInfo{{ + Name: "-1111111111111", + Account: "telegram.zzz", + Direction: "inout", + ID: "-1111111111111telegram.zzz", + SameChannel: map[string]bool{"bridge": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) case "slack.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "irc", Account: "slack.zzz", Direction: "inout", ID: "ircslack.zzz", SameChannel: map[string]bool{"bridge": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + assert.Equal(t, []config.ChannelInfo{{ + Name: "irc", + Account: "slack.zzz", + Direction: "inout", + ID: "ircslack.zzz", + SameChannel: map[string]bool{"bridge": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) } } case "bridge2": - if (msg.Channel == "#main-help" || msg.Channel == "--444444444444") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz") { + if (msg.Channel == "#main-help" || msg.Channel == "--444444444444") && + (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz") { hits[gw.Name]++ switch br.Account { case "irc.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "#main-help", Account: "irc.zzz", Direction: "inout", ID: "#main-helpirc.zzz", SameChannel: map[string]bool{"bridge2": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + assert.Equal(t, []config.ChannelInfo{{ + Name: "#main-help", + Account: "irc.zzz", + Direction: "inout", + ID: "#main-helpirc.zzz", + SameChannel: map[string]bool{"bridge2": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) case "telegram.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "--444444444444", Account: "telegram.zzz", Direction: "inout", ID: "--444444444444telegram.zzz", SameChannel: map[string]bool{"bridge2": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + assert.Equal(t, []config.ChannelInfo{{ + Name: "--444444444444", + Account: "telegram.zzz", + Direction: "inout", + ID: "--444444444444telegram.zzz", + SameChannel: map[string]bool{"bridge2": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) } } case "bridge3": - if (msg.Channel == "#main-telegram" || msg.Channel == "--333333333333") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz") { + if (msg.Channel == "#main-telegram" || msg.Channel == "--333333333333") && + (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz") { hits[gw.Name]++ switch br.Account { case "irc.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "#main-telegram", Account: "irc.zzz", Direction: "inout", ID: "#main-telegramirc.zzz", SameChannel: map[string]bool{"bridge3": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + assert.Equal(t, []config.ChannelInfo{{ + Name: "#main-telegram", + Account: "irc.zzz", + Direction: "inout", + ID: "#main-telegramirc.zzz", + SameChannel: map[string]bool{"bridge3": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) case "telegram.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "--333333333333", Account: "telegram.zzz", Direction: "inout", ID: "--333333333333telegram.zzz", SameChannel: map[string]bool{"bridge3": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + assert.Equal(t, []config.ChannelInfo{{ + Name: "--333333333333", + Account: "telegram.zzz", + Direction: "inout", + ID: "--333333333333telegram.zzz", + SameChannel: map[string]bool{"bridge3": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) } } case "announcements": @@ -265,11 +338,41 @@ func TestGetDestChannelAdvanced(t *testing.T) { hits[gw.Name]++ switch br.Account { case "irc.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "#main", Account: "irc.zzz", Direction: "out", ID: "#mainirc.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}, {Name: "#main-help", Account: "irc.zzz", Direction: "out", ID: "#main-helpirc.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + assert.Len(t, channels, 2) + assert.Contains(t, channels, config.ChannelInfo{ + Name: "#main", + Account: "irc.zzz", + Direction: "out", + ID: "#mainirc.zzz", + SameChannel: map[string]bool{"announcements": false}, + Options: config.ChannelOptions{Key: ""}, + }) + assert.Contains(t, channels, config.ChannelInfo{ + Name: "#main-help", + Account: "irc.zzz", + Direction: "out", + ID: "#main-helpirc.zzz", + SameChannel: map[string]bool{"announcements": false}, + Options: config.ChannelOptions{Key: ""}, + }) case "slack.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "general", Account: "slack.zzz", Direction: "out", ID: "generalslack.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + assert.Equal(t, []config.ChannelInfo{{ + Name: "general", + Account: "slack.zzz", + Direction: "out", + ID: "generalslack.zzz", + SameChannel: map[string]bool{"announcements": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) case "telegram.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "--333333333333", Account: "telegram.zzz", Direction: "out", ID: "--333333333333telegram.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + assert.Equal(t, []config.ChannelInfo{{ + Name: "--333333333333", + Account: "telegram.zzz", + Direction: "out", + ID: "--333333333333telegram.zzz", + SameChannel: map[string]bool{"announcements": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) } } } diff --git a/go.mod b/go.mod index 873f409e..ee50f3ab 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect github.com/nicksnyder/go-i18n v1.4.0 // indirect - github.com/nlopes/slack v0.3.1-0.20180805133408-21749ab136a8 + github.com/nlopes/slack v0.4.0 github.com/onsi/ginkgo v1.6.0 // indirect github.com/onsi/gomega v1.4.1 // indirect github.com/patcon/html2md v0.0.0-20181014143736-370b673716b2 diff --git a/go.sum b/go.sum index fae8d7bb..55d26517 100644 --- a/go.sum +++ b/go.sum @@ -101,8 +101,8 @@ github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff h1:HLGD5/9UxxfEuO9Dt github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff/go.mod h1:B8jLfIIPn2sKyWr0D7cL2v7tnrDD5z291s2Zypdu89E= github.com/nicksnyder/go-i18n v1.4.0 h1:AgLl+Yq7kg5OYlzCgu9cKTZOyI4tD/NgukKqLqC8E+I= github.com/nicksnyder/go-i18n v1.4.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= -github.com/nlopes/slack v0.3.1-0.20180805133408-21749ab136a8 h1:PSy8NkmkyldLmPPnNNw7mwfQFOHDqOI6bINpJ+/KV7Y= -github.com/nlopes/slack v0.3.1-0.20180805133408-21749ab136a8/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= +github.com/nlopes/slack v0.4.0 h1:OVnHm7lv5gGT5gkcHsZAyw++oHVFihbjWbL3UceUpiA= +github.com/nlopes/slack v0.4.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U= diff --git a/matterclient/matterclient.go b/matterclient/matterclient.go index b86fbe3d..117a2fe9 100644 --- a/matterclient/matterclient.go +++ b/matterclient/matterclient.go @@ -344,7 +344,7 @@ func (m *MMClient) parseActionPost(rmsg *Message) { data := model.PostFromJson(strings.NewReader(rmsg.Raw.Data["post"].(string))) // we don't have the user, refresh the userlist if m.GetUser(data.UserId) == nil { - m.log.Infof("User %s is not known, ignoring message %s", data) + m.log.Infof("User %s is not known, ignoring message %s", data.UserId, data.Message) return } rmsg.Username = m.GetUserName(data.UserId) @@ -843,7 +843,7 @@ func (m *MMClient) StatusLoop() { if m.OnWsConnect != nil { m.OnWsConnect() } - m.log.Debug("StatusLoop:", m.OnWsConnect) + m.log.Debugf("StatusLoop: %p", m.OnWsConnect) for { if m.WsQuit { return diff --git a/vendor/github.com/labstack/echo/Gopkg.toml b/vendor/github.com/labstack/echo/Gopkg.toml new file mode 100644 index 00000000..a24f61b9 --- /dev/null +++ b/vendor/github.com/labstack/echo/Gopkg.toml @@ -0,0 +1,87 @@ + +## Gopkg.toml example (these lines may be deleted) + +## "metadata" defines metadata about the project that could be used by other independent +## systems. The metadata defined here will be ignored by dep. +# [metadata] +# key1 = "value that convey data to other systems" +# system1-data = "value that is used by a system" +# system2-data = "value that is used by another system" + +## "required" lists a set of packages (not projects) that must be included in +## Gopkg.lock. This list is merged with the set of packages imported by the current +## project. Use it when your project needs a package it doesn't explicitly import - +## including "main" packages. +# required = ["github.com/user/thing/cmd/thing"] + +## "ignored" lists a set of packages (not projects) that are ignored when +## dep statically analyzes source code. Ignored packages can be in this project, +## or in a dependency. +# ignored = ["github.com/user/project/badpkg"] + +## Constraints are rules for how directly imported projects +## may be incorporated into the depgraph. They are respected by +## dep whether coming from the Gopkg.toml of the current project or a dependency. +# [[constraint]] +## Required: the root import path of the project being constrained. +# name = "github.com/user/project" +# +## Recommended: the version constraint to enforce for the project. +## Only one of "branch", "version" or "revision" can be specified. +# version = "1.0.0" +# branch = "master" +# revision = "abc123" +# +## Optional: an alternate location (URL or import path) for the project's source. +# source = "https://github.com/myfork/package.git" +# +## "metadata" defines metadata about the dependency or override that could be used +## by other independent systems. The metadata defined here will be ignored by dep. +# [metadata] +# key1 = "value that convey data to other systems" +# system1-data = "value that is used by a system" +# system2-data = "value that is used by another system" + +## Overrides have the same structure as [[constraint]], but supersede all +## [[constraint]] declarations from all projects. Only [[override]] from +## the current project's are applied. +## +## Overrides are a sledgehammer. Use them only as a last resort. +# [[override]] +## Required: the root import path of the project being constrained. +# name = "github.com/user/project" +# +## Optional: specifying a version constraint override will cause all other +## constraints on this project to be ignored; only the overridden constraint +## need be satisfied. +## Again, only one of "branch", "version" or "revision" can be specified. +# version = "1.0.0" +# branch = "master" +# revision = "abc123" +# +## Optional: specifying an alternate source location as an override will +## enforce that the alternate location is used for that project, regardless of +## what source location any dependent projects specify. +# source = "https://github.com/myfork/package.git" + + + +[[constraint]] + name = "github.com/dgrijalva/jwt-go" + version = "3.0.0" + +[[constraint]] + name = "github.com/labstack/gommon" + version = "0.2.1" + +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.1.4" + +[[constraint]] + branch = "master" + name = "github.com/valyala/fasttemplate" + +[[constraint]] + branch = "master" + name = "golang.org/x/crypto" diff --git a/vendor/github.com/nlopes/slack/CHANGELOG.md b/vendor/github.com/nlopes/slack/CHANGELOG.md index a79ea50c..cf0fc2cc 100644 --- a/vendor/github.com/nlopes/slack/CHANGELOG.md +++ b/vendor/github.com/nlopes/slack/CHANGELOG.md @@ -1,3 +1,15 @@ +### v0.4.0 - October 06, 2018 +full differences can be viewed using `git log --oneline --decorate --color v0.3.0..v0.4.0` +- Breaking Change: renamed ApplyMessageOption, to mark it as unsafe, +this means it may break without warning in the future. +- Breaking: Msg structure files field changed to an array. +- General: implementation for new security headers. +- RTM: deadlock fix between connect/disconnect. +- Events: various new fields added. +- Web: various fixes, new fields exposed, new methods added. +- Interactions: minor additions expect breaking changes in next release for dialogs/button clicks. +- Utils: new methods added. + ### v0.3.0 - July 30, 2018 full differences can be viewed using `git log --oneline --decorate --color v0.2.0..v0.3.0` - slack events initial support added. (still considered experimental and undergoing changes, stability not promised) diff --git a/vendor/github.com/nlopes/slack/Gopkg.lock b/vendor/github.com/nlopes/slack/Gopkg.lock index 5cc0520e..9c33d0dc 100644 --- a/vendor/github.com/nlopes/slack/Gopkg.lock +++ b/vendor/github.com/nlopes/slack/Gopkg.lock @@ -13,6 +13,12 @@ revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" version = "v1.2.0" +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + [[projects]] name = "github.com/pmezard/go-difflib" packages = ["difflib"] @@ -28,6 +34,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "888307bf47ee004aaaa4c45e6139929b4984f2253e48e382246bfb8c66f3cd65" + inputs-digest = "596fa546322c2a1e9708a10c9f39aca2e04792b477fab86fb2899fbaab776070" solver-name = "gps-cdcl" solver-version = 1 diff --git a/vendor/github.com/nlopes/slack/Gopkg.toml b/vendor/github.com/nlopes/slack/Gopkg.toml new file mode 100644 index 00000000..257870d6 --- /dev/null +++ b/vendor/github.com/nlopes/slack/Gopkg.toml @@ -0,0 +1,17 @@ +ignored = ["github.com/lusis/slack-test"] + +[[constraint]] + name = "github.com/gorilla/websocket" + version = "1.2.0" + +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.2.1" + +[[constraint]] + name = "github.com/pkg/errors" + version = "0.8.0" + +[prune] + go-tests = true + unused-packages = true diff --git a/vendor/github.com/nlopes/slack/chat.go b/vendor/github.com/nlopes/slack/chat.go index 2b89a44c..8cc6bdef 100644 --- a/vendor/github.com/nlopes/slack/chat.go +++ b/vendor/github.com/nlopes/slack/chat.go @@ -4,7 +4,8 @@ import ( "context" "encoding/json" "net/url" - "strings" + + "github.com/nlopes/slack/slackutilsx" ) const ( @@ -164,22 +165,24 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt return "", "", "", err } - if err = postSlackMethod(ctx, api.httpclient, string(config.mode), config.values, &response, api.debug); err != nil { + if err = postForm(ctx, api.httpclient, config.endpoint, config.values, &response, api.debug); err != nil { return "", "", "", err } return response.Channel, response.getMessageTimestamp(), response.Text, response.Err() } -// ApplyMsgOptions utility function for debugging/testing chat requests. -func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) { +// UnsafeApplyMsgOptions utility function for debugging/testing chat requests. +// NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this function +// will be supported by the library. +func UnsafeApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) { config, err := applyMsgOptions(token, channel, options...) - return string(config.mode), config.values, err + return config.endpoint, config.values, err } func applyMsgOptions(token, channel string, options ...MsgOption) (sendConfig, error) { config := sendConfig{ - mode: chatPostMessage, + endpoint: SLACK_API + string(chatPostMessage), values: url.Values{ "token": {token}, "channel": {channel}, @@ -195,11 +198,6 @@ func applyMsgOptions(token, channel string, options ...MsgOption) (sendConfig, e return config, nil } -func escapeMessage(message string) string { - replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") - return replacer.Replace(message) -} - type sendMode string const ( @@ -211,8 +209,8 @@ const ( ) type sendConfig struct { - mode sendMode - values url.Values + endpoint string + values url.Values } // MsgOption option provided when sending a message. @@ -221,7 +219,7 @@ type MsgOption func(*sendConfig) error // MsgOptionPost posts a messages, this is the default. func MsgOptionPost() MsgOption { return func(config *sendConfig) error { - config.mode = chatPostMessage + config.endpoint = SLACK_API + string(chatPostMessage) config.values.Del("ts") return nil } @@ -231,7 +229,7 @@ func MsgOptionPost() MsgOption { // posts an ephemeral message. func MsgOptionPostEphemeral() MsgOption { return func(config *sendConfig) error { - config.mode = chatPostEphemeral + config.endpoint = SLACK_API + string(chatPostEphemeral) config.values.Del("ts") return nil } @@ -240,7 +238,7 @@ func MsgOptionPostEphemeral() MsgOption { // MsgOptionPostEphemeral2 - posts an ephemeral message to the provided user. func MsgOptionPostEphemeral2(userID string) MsgOption { return func(config *sendConfig) error { - config.mode = chatPostEphemeral + config.endpoint = SLACK_API + string(chatPostEphemeral) MsgOptionUser(userID)(config) config.values.Del("ts") @@ -251,7 +249,7 @@ func MsgOptionPostEphemeral2(userID string) MsgOption { // MsgOptionMeMessage posts a "me message" type from the calling user func MsgOptionMeMessage() MsgOption { return func(config *sendConfig) error { - config.mode = chatMeMessage + config.endpoint = SLACK_API + string(chatMeMessage) return nil } } @@ -259,7 +257,7 @@ func MsgOptionMeMessage() MsgOption { // MsgOptionUpdate updates a message based on the timestamp. func MsgOptionUpdate(timestamp string) MsgOption { return func(config *sendConfig) error { - config.mode = chatUpdate + config.endpoint = SLACK_API + string(chatUpdate) config.values.Add("ts", timestamp) return nil } @@ -268,7 +266,7 @@ func MsgOptionUpdate(timestamp string) MsgOption { // MsgOptionDelete deletes a message based on the timestamp. func MsgOptionDelete(timestamp string) MsgOption { return func(config *sendConfig) error { - config.mode = chatDelete + config.endpoint = SLACK_API + string(chatDelete) config.values.Add("ts", timestamp) return nil } @@ -297,7 +295,7 @@ func MsgOptionUser(userID string) MsgOption { func MsgOptionText(text string, escape bool) MsgOption { return func(config *sendConfig) error { if escape { - text = escapeMessage(text) + text = slackutilsx.EscapeMessage(text) } config.values.Add("text", text) return nil @@ -392,6 +390,18 @@ func MsgOptionParse(b bool) MsgOption { } } +// UnsafeMsgOptionEndpoint deliver the message to the specified endpoint. +// NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this Option +// will be supported by the library, it is subject to change without notice that +// may result in compilation errors or runtime behaviour changes. +func UnsafeMsgOptionEndpoint(endpoint string, update func(url.Values)) MsgOption { + return func(config *sendConfig) error { + config.endpoint = endpoint + update(config.values) + return nil + } +} + // MsgOptionPostMessageParameters maintain backwards compatibility. func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { return func(config *sendConfig) error { diff --git a/vendor/github.com/nlopes/slack/conversation.go b/vendor/github.com/nlopes/slack/conversation.go index edde87a2..1c64116e 100644 --- a/vendor/github.com/nlopes/slack/conversation.go +++ b/vendor/github.com/nlopes/slack/conversation.go @@ -29,6 +29,8 @@ type conversation struct { NameNormalized string `json:"name_normalized"` NumMembers int `json:"num_members"` Priority float64 `json:"priority"` + User string `json:"user"` + // TODO support pending_shared // TODO support previous_names } @@ -64,6 +66,13 @@ type GetUsersInConversationParameters struct { Limit int } +type GetConversationsForUserParameters struct { + UserID string + Cursor string + Types []string + Limit int +} + type responseMetaData struct { NextCursor string `json:"next_cursor"` } @@ -100,6 +109,41 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge return response.Members, response.ResponseMetaData.NextCursor, nil } +// GetConversationsForUser returns the list conversations for a given user +func (api *Client) GetConversationsForUser(params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) { + return api.GetConversationsForUserContext(context.Background(), params) +} + +// GetConversationsForUserContext returns the list conversations for a given user with a custom context +func (api *Client) GetConversationsForUserContext(ctx context.Context, params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) { + values := url.Values{ + "token": {api.token}, + "user": {params.UserID}, + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Types != nil { + values.Add("types", strings.Join(params.Types, ",")) + } + response := struct { + Channels []Channel `json:"channels"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + err = postSlackMethod(ctx, api.httpclient, "users.conversations", values, &response, api.debug) + if err != nil { + return nil, "", err + } + if !response.Ok { + return nil, "", errors.New(response.Error) + } + return response.Channels, response.ResponseMetaData.NextCursor, nil +} + // ArchiveConversation archives a conversation func (api *Client) ArchiveConversation(channelID string) error { return api.ArchiveConversationContext(context.Background(), channelID) diff --git a/vendor/github.com/nlopes/slack/dialog.go b/vendor/github.com/nlopes/slack/dialog.go index a13e53da..d1435d54 100644 --- a/vendor/github.com/nlopes/slack/dialog.go +++ b/vendor/github.com/nlopes/slack/dialog.go @@ -6,52 +6,47 @@ import ( "errors" ) +// InputType is the type of the dialog input type +type InputType string + +const ( + // InputTypeText textfield input + InputTypeText InputType = "text" + // InputTypeTextArea textarea input + InputTypeTextArea InputType = "textarea" + // InputTypeSelect textfield input + InputTypeSelect InputType = "select" +) + +// DialogInput for dialogs input type text or menu +type DialogInput struct { + Type InputType `json:"type"` + Label string `json:"label"` + Name string `json:"name"` + Placeholder string `json:"placeholder"` + Optional bool `json:"optional"` +} + +// DialogTrigger ... type DialogTrigger struct { - TriggerId string `json:"trigger_id"` //Required. Must respond within 3 seconds. + TriggerID string `json:"trigger_id"` //Required. Must respond within 3 seconds. Dialog Dialog `json:"dialog"` //Required. } +// Dialog as in Slack dialogs +// https://api.slack.com/dialogs#option_element_attributes#top-level_dialog_attributes type Dialog struct { - CallbackId string `json:"callback_id"` //Required. - Title string `json:"title"` //Required. - SubmitLabel string `json:"submit_label,omitempty"` //Optional. Default value is 'Submit' - NotifyOnCancel bool `json:"notify_on_cancel,omitempty"` //Optional. Default value is false - Elements []DialogElement `json:"elements"` //Required. + TriggerID string `json:"trigger_id"` //Required + CallbackID string `json:"callback_id"` //Required + Title string `json:"title"` + SubmitLabel string `json:"submit_label,omitempty"` + NotifyOnCancel bool `json:"notify_on_cancel"` + Elements []DialogElement `json:"elements"` } +// DialogElement abstract type for dialogs. type DialogElement interface{} -type DialogTextElement struct { - Label string `json:"label"` //Required. - Name string `json:"name"` //Required. - Type string `json:"type"` //Required. Allowed values: "text", "textarea", "select". - Placeholder string `json:"placeholder,omitempty"` //Optional. - Optional bool `json:"optional,omitempty"` //Optional. Default value is false - Value string `json:"value,omitempty"` //Optional. - MaxLength int `json:"max_length,omitempty"` //Optional. - MinLength int `json:"min_length,omitempty"` //Optional,. Default value is 0 - Hint string `json:"hint,omitempty"` //Optional. - Subtype string `json:"subtype,omitempty"` //Optional. Allowed values: "email", "number", "tel", "url". -} - -type DialogSelectElement struct { - Label string `json:"label"` //Required. - Name string `json:"name"` //Required. - Type string `json:"type"` //Required. Allowed values: "text", "textarea", "select". - Placeholder string `json:"placeholder,omitempty"` //Optional. - Optional bool `json:"optional,omitempty"` //Optional. Default value is false - Value string `json:"value,omitempty"` //Optional. - DataSource string `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external". - SelectedOptions string `json:"selected_options,omitempty"` //Optional. Default value for "external" only - Options []DialogElementOption `json:"options,omitempty"` //One of options or option_groups is required. - OptionGroups []DialogElementOption `json:"option_groups,omitempty"` //Provide up to 100 options. -} - -type DialogElementOption struct { - Label string `json:"label"` //Required. - Value string `json:"value"` //Required. -} - // DialogCallback is sent from Slack when a user submits a form from within a dialog type DialogCallback struct { Type string `json:"type"` @@ -78,28 +73,43 @@ type DialogSuggestionCallback struct { CallbackID string `json:"callback_id"` } -// OpenDialog opens a dialog window where the triggerId originated from -func (api *Client) OpenDialog(triggerId string, dialog Dialog) (err error) { - return api.OpenDialogContext(context.Background(), triggerId, dialog) +// DialogOpenResponse response from `dialog.open` +type DialogOpenResponse struct { + SlackResponse + DialogResponseMetadata DialogResponseMetadata `json:"response_metadata"` +} + +// DialogResponseMetadata lists the error messages +type DialogResponseMetadata struct { + Messages []string `json:"messages"` +} + +// OpenDialog opens a dialog window where the triggerID originated from. +// EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable. +func (api *Client) OpenDialog(triggerID string, dialog Dialog) (err error) { + return api.OpenDialogContext(context.Background(), triggerID, dialog) } // OpenDialogContext opens a dialog window where the triggerId originated from with a custom context -func (api *Client) OpenDialogContext(ctx context.Context, triggerId string, dialog Dialog) (err error) { - if triggerId == "" { +// EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable. +func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dialog Dialog) (err error) { + if triggerID == "" { return errors.New("received empty parameters") } - resp := DialogTrigger{ - TriggerId: triggerId, + req := DialogTrigger{ + TriggerID: triggerID, Dialog: dialog, } - jsonResp, err := json.Marshal(resp) + + encoded, err := json.Marshal(req) if err != nil { return err } - response := &SlackResponse{} + + response := &DialogOpenResponse{} endpoint := SLACK_API + "dialog.open" - if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonResp, response, api.debug); err != nil { + if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api.debug); err != nil { return err } diff --git a/vendor/github.com/nlopes/slack/dialog_select.go b/vendor/github.com/nlopes/slack/dialog_select.go new file mode 100644 index 00000000..cff35479 --- /dev/null +++ b/vendor/github.com/nlopes/slack/dialog_select.go @@ -0,0 +1,125 @@ +package slack + +// SelectDataSource types of select datasource +type SelectDataSource string + +const ( + // DialogDataSourceStatic menu with static Options/OptionGroups + DialogDataSourceStatic SelectDataSource = "static" + // DialogDataSourceExternal dynamic datasource + DialogDataSourceExternal SelectDataSource = "external" + // DialogDataSourceConversations provides a list of conversations + DialogDataSourceConversations SelectDataSource = "conversations" + // DialogDataSourceChannels provides a list of channels + DialogDataSourceChannels SelectDataSource = "channels" + // DialogDataSourceUsers provides a list of users + DialogDataSourceUsers SelectDataSource = "users" +) + +// DialogInputSelect dialog support for select boxes. +type DialogInputSelect struct { + DialogInput + Value string `json:"value,omitempty"` //Optional. + DataSource SelectDataSource `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external". + SelectedOptions string `json:"selected_options,omitempty"` //Optional. Default value for "external" only + Options []DialogSelectOption `json:"options,omitempty"` //One of options or option_groups is required. + OptionGroups []DialogOptionGroup `json:"option_groups,omitempty"` //Provide up to 100 options. +} + +// DialogSelectOption is an option for the user to select from the menu +type DialogSelectOption struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// DialogOptionGroup is a collection of options for creating a segmented table +type DialogOptionGroup struct { + Label string `json:"label"` + Options []DialogSelectOption `json:"options"` +} + +// NewStaticSelectDialogInput constructor for a `static` datasource menu input +func NewStaticSelectDialogInput(name, label string, options []DialogSelectOption) *DialogInputSelect { + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Name: name, + Label: label, + Optional: true, + }, + DataSource: DialogDataSourceStatic, + Options: options, + } +} + +// NewGroupedSelectDialogInput creates grouped options select input for Dialogs. +func NewGroupedSelectDialogInput(name, label string, groups map[string]map[string]string) *DialogInputSelect { + optionGroups := []DialogOptionGroup{} + for groupName, options := range groups { + optionGroups = append(optionGroups, DialogOptionGroup{ + Label: groupName, + Options: optionsFromMap(options), + }) + } + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Name: name, + Label: label, + }, + DataSource: DialogDataSourceStatic, + OptionGroups: optionGroups, + } +} + +func optionsFromArray(options []string) []DialogSelectOption { + selectOptions := make([]DialogSelectOption, len(options)) + for idx, value := range options { + selectOptions[idx] = DialogSelectOption{ + Label: value, + Value: value, + } + } + return selectOptions +} + +func optionsFromMap(options map[string]string) []DialogSelectOption { + selectOptions := make([]DialogSelectOption, len(options)) + idx := 0 + var option DialogSelectOption + for key, value := range options { + option = DialogSelectOption{ + Label: key, + Value: value, + } + selectOptions[idx] = option + idx++ + } + return selectOptions +} + +// NewConversationsSelect returns a `Conversations` select +func NewConversationsSelect(name, label string) *DialogInputSelect { + return newPresetSelect(name, label, DialogDataSourceConversations) +} + +// NewChannelsSelect returns a `Channels` select +func NewChannelsSelect(name, label string) *DialogInputSelect { + return newPresetSelect(name, label, DialogDataSourceChannels) +} + +// NewUsersSelect returns a `Users` select +func NewUsersSelect(name, label string) *DialogInputSelect { + return newPresetSelect(name, label, DialogDataSourceUsers) +} + +func newPresetSelect(name, label string, dataSourceType SelectDataSource) *DialogInputSelect { + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Label: label, + Name: name, + }, + DataSource: dataSourceType, + } +} diff --git a/vendor/github.com/nlopes/slack/dialog_text.go b/vendor/github.com/nlopes/slack/dialog_text.go new file mode 100644 index 00000000..bf9602cc --- /dev/null +++ b/vendor/github.com/nlopes/slack/dialog_text.go @@ -0,0 +1,50 @@ +package slack + +// TextInputSubtype Accepts email, number, tel, or url. In some form factors, optimized input is provided for this subtype. +type TextInputSubtype string + +const ( + // InputSubtypeEmail email keyboard + InputSubtypeEmail TextInputSubtype = "email" + // InputSubtypeNumber numeric keyboard + InputSubtypeNumber TextInputSubtype = "number" + // InputSubtypeTel Phone keyboard + InputSubtypeTel TextInputSubtype = "tel" + // InputSubtypeURL Phone keyboard + InputSubtypeURL TextInputSubtype = "url" +) + +// TextInputElement subtype of DialogInput +// https://api.slack.com/dialogs#option_element_attributes#text_element_attributes +type TextInputElement struct { + DialogInput + MaxLength int `json:"max_length,omitempty"` + MinLength int `json:"min_length,omitempty"` + Hint string `json:"hint,omitempty"` + Subtype TextInputSubtype `json:"subtype"` + Value string `json:"value"` +} + +// NewTextInput constructor for a `text` input +func NewTextInput(name, label, text string) *TextInputElement { + return &TextInputElement{ + DialogInput: DialogInput{ + Type: InputTypeText, + Name: name, + Label: label, + }, + Value: text, + } +} + +// NewTextAreaInput constructor for a `textarea` input +func NewTextAreaInput(name, label, text string) *TextInputElement { + return &TextInputElement{ + DialogInput: DialogInput{ + Type: InputTypeTextArea, + Name: name, + Label: label, + }, + Value: text, + } +} diff --git a/vendor/github.com/nlopes/slack/files.go b/vendor/github.com/nlopes/slack/files.go index 2381ec3c..0550c9fb 100644 --- a/vendor/github.com/nlopes/slack/files.go +++ b/vendor/github.com/nlopes/slack/files.go @@ -93,14 +93,15 @@ type File struct { // There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large, // or provide a local file path in File to upload it from your filesystem. type FileUploadParameters struct { - File string - Content string - Reader io.Reader - Filetype string - Filename string - Title string - InitialComment string - Channels []string + File string + Content string + Reader io.Reader + Filetype string + Filename string + Title string + InitialComment string + Channels []string + ThreadTimestamp string } // GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request @@ -237,6 +238,9 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam if params.InitialComment != "" { values.Add("initial_comment", params.InitialComment) } + if params.ThreadTimestamp != "" { + values.Add("thread_ts", params.ThreadTimestamp) + } if len(params.Channels) != 0 { values.Add("channels", strings.Join(params.Channels, ",")) } diff --git a/vendor/github.com/nlopes/slack/security.go b/vendor/github.com/nlopes/slack/security.go new file mode 100644 index 00000000..50201d99 --- /dev/null +++ b/vendor/github.com/nlopes/slack/security.go @@ -0,0 +1,47 @@ +package slack + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "hash" + "net/http" +) + +// SecretsVerifier contains the information needed to verify that the request comes from Slack +type SecretsVerifier struct { + slackSig string + timeStamp string + hmac hash.Hash +} + +// NewSecretsVerifier returns a SecretsVerifier object in exchange for an http.Header object and signing secret +func NewSecretsVerifier(header http.Header, signingSecret string) (SecretsVerifier, error) { + if header["X-Slack-Signature"][0] == "" || header["X-Slack-Request-Timestamp"][0] == "" { + return SecretsVerifier{}, errors.New("Headers are empty, cannot create SecretsVerifier") + } + + hash := hmac.New(sha256.New, []byte(signingSecret)) + hash.Write([]byte(fmt.Sprintf("v0:%s:", header["X-Slack-Request-Timestamp"][0]))) + return SecretsVerifier{ + slackSig: header["X-Slack-Signature"][0], + timeStamp: header["X-Slack-Request-Timestamp"][0], + hmac: hash, + }, nil +} + +func (v *SecretsVerifier) Write(body []byte) (n int, err error) { + return v.hmac.Write(body) +} + +// Ensure compares the signature sent from Slack with the actual computed hash to judge validity +func (v SecretsVerifier) Ensure() error { + computed := "v0=" + string(hex.EncodeToString(v.hmac.Sum(nil))) + if computed == v.slackSig { + return nil + } + + return fmt.Errorf("Expected signing signature: %s, but computed: %s", v.slackSig, computed) +} diff --git a/vendor/github.com/nlopes/slack/slackutilsx/slackutilsx.go b/vendor/github.com/nlopes/slack/slackutilsx/slackutilsx.go new file mode 100644 index 00000000..ccf5372b --- /dev/null +++ b/vendor/github.com/nlopes/slack/slackutilsx/slackutilsx.go @@ -0,0 +1,57 @@ +// Package slackutilsx is a utility package that doesn't promise API stability. +// its for experimental functionality and utilities. +package slackutilsx + +import ( + "strings" + "unicode/utf8" +) + +// ChannelType the type of channel based on the channelID +type ChannelType int + +func (t ChannelType) String() string { + switch t { + case CTypeDM: + return "Direct" + case CTypeGroup: + return "Group" + case CTypeChannel: + return "Channel" + default: + return "Unknown" + } +} + +const ( + // CTypeUnknown represents channels we cannot properly detect. + CTypeUnknown ChannelType = iota + // CTypeDM is a private channel between two slack users. + CTypeDM + // CTypeGroup is a group channel. + CTypeGroup + // CTypeChannel is a public channel. + CTypeChannel +) + +// DetectChannelType converts a channelID to a ChannelType. +// channelID must not be empty. However, if it is empty, the channel type will default to Unknown. +func DetectChannelType(channelID string) ChannelType { + // intentionally ignore the error and just default to CTypeUnknown + switch r, _ := utf8.DecodeRuneInString(channelID); r { + case 'C': + return CTypeChannel + case 'G': + return CTypeGroup + case 'D': + return CTypeDM + default: + return CTypeUnknown + } +} + +// EscapeMessage text +func EscapeMessage(message string) string { + replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") + return replacer.Replace(message) +} diff --git a/vendor/github.com/nlopes/slack/usergroups.go b/vendor/github.com/nlopes/slack/usergroups.go index 1e2b6442..cc9bc4ca 100644 --- a/vendor/github.com/nlopes/slack/usergroups.go +++ b/vendor/github.com/nlopes/slack/usergroups.go @@ -25,6 +25,7 @@ type UserGroup struct { DeletedBy string `json:"deleted_by"` Prefs UserGroupPrefs `json:"prefs"` UserCount int `json:"user_count"` + Users []string `json:"users"` } // UserGroupPrefs contains default channels and groups (private channels) @@ -121,16 +122,62 @@ func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) return response.UserGroup, nil } +// GetUserGroupsOption options for the GetUserGroups method call. +type GetUserGroupsOption func(*GetUserGroupsParams) + +// GetUserGroupsOptionIncludeCount include the number of users in each User Group (default: false) +func GetUserGroupsOptionIncludeCount(b bool) GetUserGroupsOption { + return func(params *GetUserGroupsParams) { + params.IncludeCount = b + } +} + +// GetUserGroupsOptionIncludeDisabled include disabled User Groups (default: false) +func GetUserGroupsOptionIncludeDisabled(b bool) GetUserGroupsOption { + return func(params *GetUserGroupsParams) { + params.IncludeDisabled = b + } +} + +// GetUserGroupsOptionIncludeUsers include the list of users for each User Group (default: false) +func GetUserGroupsOptionIncludeUsers(b bool) GetUserGroupsOption { + return func(params *GetUserGroupsParams) { + params.IncludeUsers = b + } +} + +// GetUserGroupsParams contains arguments for GetUserGroups method call +type GetUserGroupsParams struct { + IncludeCount bool + IncludeDisabled bool + IncludeUsers bool +} + // GetUserGroups returns a list of user groups for the team -func (api *Client) GetUserGroups() ([]UserGroup, error) { - return api.GetUserGroupsContext(context.Background()) +func (api *Client) GetUserGroups(options ...GetUserGroupsOption) ([]UserGroup, error) { + return api.GetUserGroupsContext(context.Background(), options...) } // GetUserGroupsContext returns a list of user groups for the team with a custom context -func (api *Client) GetUserGroupsContext(ctx context.Context) ([]UserGroup, error) { +func (api *Client) GetUserGroupsContext(ctx context.Context, options ...GetUserGroupsOption) ([]UserGroup, error) { + params := GetUserGroupsParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, } + if params.IncludeCount { + values.Add("include_count", "true") + } + if params.IncludeDisabled { + values.Add("include_disabled", "true") + } + if params.IncludeUsers { + values.Add("include_users", "true") + } response, err := userGroupRequest(ctx, api.httpclient, "usergroups.list", values, api.debug) if err != nil { diff --git a/vendor/github.com/nlopes/slack/webhooks.go b/vendor/github.com/nlopes/slack/webhooks.go index 870a8d8b..3ea69ffe 100644 --- a/vendor/github.com/nlopes/slack/webhooks.go +++ b/vendor/github.com/nlopes/slack/webhooks.go @@ -1,15 +1,16 @@ package slack import ( - "github.com/pkg/errors" - "net/http" "bytes" "encoding/json" + "net/http" + + "github.com/pkg/errors" ) type WebhookMessage struct { - Text string `json:"text,omitempty"` - Attachments []Attachment `json:"attachments,omitempty"` + Text string `json:"text,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` } func PostWebhook(url string, msg *WebhookMessage) error { @@ -19,7 +20,7 @@ func PostWebhook(url string, msg *WebhookMessage) error { return errors.Wrap(err, "marshal failed") } - response, err := http.Post(url, "application/json", bytes.NewReader(raw)); + response, err := http.Post(url, "application/json", bytes.NewReader(raw)) if err != nil { return errors.Wrap(err, "failed to post webhook") diff --git a/vendor/github.com/nlopes/slack/websocket_managed_conn.go b/vendor/github.com/nlopes/slack/websocket_managed_conn.go index b6d1bfc8..e8ab65a1 100644 --- a/vendor/github.com/nlopes/slack/websocket_managed_conn.go +++ b/vendor/github.com/nlopes/slack/websocket_managed_conn.go @@ -524,4 +524,9 @@ var EventMapping = map[string]interface{}{ "member_joined_channel": MemberJoinedChannelEvent{}, "member_left_channel": MemberLeftChannelEvent{}, + + "subteam_created": SubteamCreatedEvent{}, + "subteam_self_added": SubteamSelfAddedEvent{}, + "subteam_self_removed": SubteamSelfRemovedEvent{}, + "subteam_updated": SubteamUpdatedEvent{}, } diff --git a/vendor/github.com/nlopes/slack/websocket_subteam.go b/vendor/github.com/nlopes/slack/websocket_subteam.go new file mode 100644 index 00000000..a23b274c --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_subteam.go @@ -0,0 +1,35 @@ +package slack + +// SubteamCreatedEvent represents the Subteam created event +type SubteamCreatedEvent struct { + Type string `json:"type"` + Subteam UserGroup `json:"subteam"` +} + +// SubteamCreatedEvent represents the membership of an existing User Group has changed event +type SubteamMembersChangedEvent struct { + Type string `json:"type"` + SubteamID string `json:"subteam_id"` + TeamID string `json:"team_id"` + DatePreviousUpdate JSONTime `json:"date_previous_update"` + DateUpdate JSONTime `json:"date_update"` + AddedUsers []string `json:"added_users"` + AddedUsersCount string `json:"added_users_count"` + RemovedUsers []string `json:"removed_users"` + RemovedUsersCount string `json:"removed_users_count"` +} + +// SubteamSelfAddedEvent represents an event of you have been added to a User Group +type SubteamSelfAddedEvent struct { + Type string `json:"type"` + SubteamID string `json:"subteam_id"` +} + +// SubteamSelfRemovedEvent represents an event of you have been removed from a User Group +type SubteamSelfRemovedEvent SubteamSelfAddedEvent + +// SubteamUpdatedEvent represents an event of an existing User Group has been updated or its members changed +type SubteamUpdatedEvent struct { + Type string `json:"type"` + Subteam UserGroup `json:"subteam"` +} diff --git a/vendor/github.com/pelletier/go-toml/benchmark.toml b/vendor/github.com/pelletier/go-toml/benchmark.toml new file mode 100644 index 00000000..dfd77e09 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/benchmark.toml @@ -0,0 +1,244 @@ +################################################################################ +## Comment + +# Speak your mind with the hash symbol. They go from the symbol to the end of +# the line. + + +################################################################################ +## Table + +# Tables (also known as hash tables or dictionaries) are collections of +# key/value pairs. They appear in square brackets on a line by themselves. + +[table] + +key = "value" # Yeah, you can do this. + +# Nested tables are denoted by table names with dots in them. Name your tables +# whatever crap you please, just don't use #, ., [ or ]. + +[table.subtable] + +key = "another value" + +# You don't need to specify all the super-tables if you don't want to. TOML +# knows how to do it for you. + +# [x] you +# [x.y] don't +# [x.y.z] need these +[x.y.z.w] # for this to work + + +################################################################################ +## Inline Table + +# Inline tables provide a more compact syntax for expressing tables. They are +# especially useful for grouped data that can otherwise quickly become verbose. +# Inline tables are enclosed in curly braces `{` and `}`. No newlines are +# allowed between the curly braces unless they are valid within a value. + +[table.inline] + +name = { first = "Tom", last = "Preston-Werner" } +point = { x = 1, y = 2 } + + +################################################################################ +## String + +# There are four ways to express strings: basic, multi-line basic, literal, and +# multi-line literal. All strings must contain only valid UTF-8 characters. + +[string.basic] + +basic = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF." + +[string.multiline] + +# The following strings are byte-for-byte equivalent: +key1 = "One\nTwo" +key2 = """One\nTwo""" +key3 = """ +One +Two""" + +[string.multiline.continued] + +# The following strings are byte-for-byte equivalent: +key1 = "The quick brown fox jumps over the lazy dog." + +key2 = """ +The quick brown \ + + + fox jumps over \ + the lazy dog.""" + +key3 = """\ + The quick brown \ + fox jumps over \ + the lazy dog.\ + """ + +[string.literal] + +# What you see is what you get. +winpath = 'C:\Users\nodejs\templates' +winpath2 = '\\ServerX\admin$\system32\' +quoted = 'Tom "Dubs" Preston-Werner' +regex = '<\i\c*\s*>' + + +[string.literal.multiline] + +regex2 = '''I [dw]on't need \d{2} apples''' +lines = ''' +The first newline is +trimmed in raw strings. + All other whitespace + is preserved. +''' + + +################################################################################ +## Integer + +# Integers are whole numbers. Positive numbers may be prefixed with a plus sign. +# Negative numbers are prefixed with a minus sign. + +[integer] + +key1 = +99 +key2 = 42 +key3 = 0 +key4 = -17 + +[integer.underscores] + +# For large numbers, you may use underscores to enhance readability. Each +# underscore must be surrounded by at least one digit. +key1 = 1_000 +key2 = 5_349_221 +key3 = 1_2_3_4_5 # valid but inadvisable + + +################################################################################ +## Float + +# A float consists of an integer part (which may be prefixed with a plus or +# minus sign) followed by a fractional part and/or an exponent part. + +[float.fractional] + +key1 = +1.0 +key2 = 3.1415 +key3 = -0.01 + +[float.exponent] + +key1 = 5e+22 +key2 = 1e6 +key3 = -2E-2 + +[float.both] + +key = 6.626e-34 + +[float.underscores] + +key1 = 9_224_617.445_991_228_313 +key2 = 1e1_00 + + +################################################################################ +## Boolean + +# Booleans are just the tokens you're used to. Always lowercase. + +[boolean] + +True = true +False = false + + +################################################################################ +## Datetime + +# Datetimes are RFC 3339 dates. + +[datetime] + +key1 = 1979-05-27T07:32:00Z +key2 = 1979-05-27T00:32:00-07:00 +key3 = 1979-05-27T00:32:00.999999-07:00 + + +################################################################################ +## Array + +# Arrays are square brackets with other primitives inside. Whitespace is +# ignored. Elements are separated by commas. Data types may not be mixed. + +[array] + +key1 = [ 1, 2, 3 ] +key2 = [ "red", "yellow", "green" ] +key3 = [ [ 1, 2 ], [3, 4, 5] ] +#key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok + +# Arrays can also be multiline. So in addition to ignoring whitespace, arrays +# also ignore newlines between the brackets. Terminating commas are ok before +# the closing bracket. + +key5 = [ + 1, 2, 3 +] +key6 = [ + 1, + 2, # this is ok +] + + +################################################################################ +## Array of Tables + +# These can be expressed by using a table name in double brackets. Each table +# with the same double bracketed name will be an element in the array. The +# tables are inserted in the order encountered. + +[[products]] + +name = "Hammer" +sku = 738594937 + +[[products]] + +[[products]] + +name = "Nail" +sku = 284758393 +color = "gray" + + +# You can create nested arrays of tables as well. + +[[fruit]] + name = "apple" + + [fruit.physical] + color = "red" + shape = "round" + + [[fruit.variety]] + name = "red delicious" + + [[fruit.variety]] + name = "granny smith" + +[[fruit]] + name = "banana" + + [[fruit.variety]] + name = "plantain" diff --git a/vendor/github.com/pelletier/go-toml/example-crlf.toml b/vendor/github.com/pelletier/go-toml/example-crlf.toml new file mode 100644 index 00000000..12950a16 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/example-crlf.toml @@ -0,0 +1,29 @@ +# This is a TOML document. Boom. + +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" +organization = "GitHub" +bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." +dob = 1979-05-27T07:32:00Z # First class dates? Why not? + +[database] +server = "192.168.1.1" +ports = [ 8001, 8001, 8002 ] +connection_max = 5000 +enabled = true + +[servers] + + # You can indent as you please. Tabs or spaces. TOML don't care. + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +[clients] +data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it diff --git a/vendor/github.com/pelletier/go-toml/example.toml b/vendor/github.com/pelletier/go-toml/example.toml new file mode 100644 index 00000000..3d902f28 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/example.toml @@ -0,0 +1,29 @@ +# This is a TOML document. Boom. + +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" +organization = "GitHub" +bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." +dob = 1979-05-27T07:32:00Z # First class dates? Why not? + +[database] +server = "192.168.1.1" +ports = [ 8001, 8001, 8002 ] +connection_max = 5000 +enabled = true + +[servers] + + # You can indent as you please. Tabs or spaces. TOML don't care. + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +[clients] +data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it diff --git a/vendor/github.com/pelletier/go-toml/marshal_test.toml b/vendor/github.com/pelletier/go-toml/marshal_test.toml new file mode 100644 index 00000000..1c5f98e7 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/marshal_test.toml @@ -0,0 +1,38 @@ +title = "TOML Marshal Testing" + +[basic] + bool = true + date = 1979-05-27T07:32:00Z + float = 123.4 + int = 5000 + string = "Bite me" + uint = 5001 + +[basic_lists] + bools = [true,false,true] + dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z] + floats = [12.3,45.6,78.9] + ints = [8001,8001,8002] + strings = ["One","Two","Three"] + uints = [5002,5003] + +[basic_map] + one = "one" + two = "two" + +[subdoc] + + [subdoc.first] + name = "First" + + [subdoc.second] + name = "Second" + +[[subdoclist]] + name = "List.First" + +[[subdoclist]] + name = "List.Second" + +[[subdocptrs]] + name = "Second" diff --git a/vendor/modules.txt b/vendor/modules.txt index c95b593e..9a59b2ce 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -95,8 +95,9 @@ github.com/nicksnyder/go-i18n/i18n github.com/nicksnyder/go-i18n/i18n/bundle github.com/nicksnyder/go-i18n/i18n/language github.com/nicksnyder/go-i18n/i18n/translation -# github.com/nlopes/slack v0.3.1-0.20180805133408-21749ab136a8 +# github.com/nlopes/slack v0.4.0 github.com/nlopes/slack +github.com/nlopes/slack/slackutilsx # github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83 github.com/paulrosania/go-charset/charset github.com/paulrosania/go-charset/data