Merge branch 'master' into 499-google-translation

This commit is contained in:
Patrick Connolly
2018-10-15 07:09:37 +08:00
32 changed files with 1721 additions and 644 deletions

View File

@@ -1,8 +1,6 @@
language: go
go:
#- 1.7.x
- 1.10.x
# - tip
- 1.11.x
# we have everything vendored
install: true

View File

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

353
bridge/slack/handlers.go Normal file
View File

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

113
bridge/slack/helpers.go Normal file
View File

@@ -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(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
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
}

View File

@@ -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 <ID>", 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(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`).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 ""
}

View File

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

View File

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

View File

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

2
go.mod
View File

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

4
go.sum
View File

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

View File

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

87
vendor/github.com/labstack/echo/Gopkg.toml generated vendored Normal file
View File

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

View File

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

View File

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

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

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

View File

@@ -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("&", "&amp;", "<", "&lt;", ">", "&gt;")
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 {

View File

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

View File

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

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

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

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

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

View File

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

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

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

View File

@@ -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("&", "&amp;", "<", "&lt;", ">", "&gt;")
return replacer.Replace(message)
}

View File

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

View File

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

View File

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

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

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

244
vendor/github.com/pelletier/go-toml/benchmark.toml generated vendored Normal file
View File

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

29
vendor/github.com/pelletier/go-toml/example-crlf.toml generated vendored Normal file
View File

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

29
vendor/github.com/pelletier/go-toml/example.toml generated vendored Normal file
View File

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

38
vendor/github.com/pelletier/go-toml/marshal_test.toml generated vendored Normal file
View File

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

3
vendor/modules.txt vendored
View File

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