mirror of
https://github.com/42wim/matterbridge.git
synced 2024-12-22 11:12:44 +01:00
ed01820722
Currently fully support mattermost,slack and discord. Message deleted on the bridge or received from other bridges will be deleted. Partially support for Gitter. Gitter bridge will delete messages received from other bridges. But if you delete a message on gitter, this deletion will not be sent to other bridges (this is a gitter API limitation, it doesn't propogate edits or deletes via the API)
392 lines
10 KiB
Go
392 lines
10 KiB
Go
package bslack
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"github.com/42wim/matterbridge/bridge/config"
|
|
"github.com/42wim/matterbridge/matterhook"
|
|
log "github.com/Sirupsen/logrus"
|
|
"github.com/nlopes/slack"
|
|
"html"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type MMMessage struct {
|
|
Text string
|
|
Channel string
|
|
Username string
|
|
UserID string
|
|
Raw *slack.MessageEvent
|
|
}
|
|
|
|
type Bslack struct {
|
|
mh *matterhook.Client
|
|
sc *slack.Client
|
|
Config *config.Protocol
|
|
rtm *slack.RTM
|
|
Plus bool
|
|
Remote chan config.Message
|
|
Users []slack.User
|
|
Account string
|
|
si *slack.Info
|
|
channels []slack.Channel
|
|
}
|
|
|
|
var flog *log.Entry
|
|
var protocol = "slack"
|
|
|
|
func init() {
|
|
flog = log.WithFields(log.Fields{"module": protocol})
|
|
}
|
|
|
|
func New(cfg config.Protocol, account string, c chan config.Message) *Bslack {
|
|
b := &Bslack{}
|
|
b.Config = &cfg
|
|
b.Remote = c
|
|
b.Account = account
|
|
return b
|
|
}
|
|
|
|
func (b *Bslack) Command(cmd string) string {
|
|
return ""
|
|
}
|
|
|
|
func (b *Bslack) Connect() error {
|
|
if b.Config.WebhookBindAddress != "" {
|
|
if b.Config.WebhookURL != "" {
|
|
flog.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
|
|
b.mh = matterhook.New(b.Config.WebhookURL,
|
|
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
|
|
BindAddress: b.Config.WebhookBindAddress})
|
|
} else if b.Config.Token != "" {
|
|
flog.Info("Connecting using token (sending)")
|
|
b.sc = slack.New(b.Config.Token)
|
|
b.rtm = b.sc.NewRTM()
|
|
go b.rtm.ManageConnection()
|
|
flog.Info("Connecting using webhookbindaddress (receiving)")
|
|
b.mh = matterhook.New(b.Config.WebhookURL,
|
|
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
|
|
BindAddress: b.Config.WebhookBindAddress})
|
|
} else {
|
|
flog.Info("Connecting using webhookbindaddress (receiving)")
|
|
b.mh = matterhook.New(b.Config.WebhookURL,
|
|
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
|
|
BindAddress: b.Config.WebhookBindAddress})
|
|
}
|
|
go b.handleSlack()
|
|
return nil
|
|
}
|
|
if b.Config.WebhookURL != "" {
|
|
flog.Info("Connecting using webhookurl (sending)")
|
|
b.mh = matterhook.New(b.Config.WebhookURL,
|
|
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
|
|
DisableServer: true})
|
|
if b.Config.Token != "" {
|
|
flog.Info("Connecting using token (receiving)")
|
|
b.sc = slack.New(b.Config.Token)
|
|
b.rtm = b.sc.NewRTM()
|
|
go b.rtm.ManageConnection()
|
|
go b.handleSlack()
|
|
}
|
|
} else if b.Config.Token != "" {
|
|
flog.Info("Connecting using token (sending and receiving)")
|
|
b.sc = slack.New(b.Config.Token)
|
|
b.rtm = b.sc.NewRTM()
|
|
go b.rtm.ManageConnection()
|
|
go b.handleSlack()
|
|
}
|
|
if b.Config.WebhookBindAddress == "" && b.Config.WebhookURL == "" && b.Config.Token == "" {
|
|
return errors.New("No connection method found. See that you have WebhookBindAddress, WebhookURL or Token configured.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Bslack) Disconnect() error {
|
|
return nil
|
|
|
|
}
|
|
|
|
func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
|
|
// we can only join channels using the API
|
|
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" {
|
|
if strings.HasPrefix(b.Config.Token, "xoxb") {
|
|
// TODO check if bot has already joined channel
|
|
return nil
|
|
}
|
|
_, err := b.sc.JoinChannel(channel.Name)
|
|
if err != nil {
|
|
if err.Error() != "name_taken" {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Bslack) Send(msg config.Message) (string, error) {
|
|
flog.Debugf("Receiving %#v", msg)
|
|
if msg.Event == config.EVENT_USER_ACTION {
|
|
msg.Text = "_" + msg.Text + "_"
|
|
}
|
|
nick := msg.Username
|
|
message := msg.Text
|
|
channel := msg.Channel
|
|
if b.Config.PrefixMessagesWithNick {
|
|
message = nick + " " + message
|
|
}
|
|
if b.Config.WebhookURL != "" {
|
|
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
|
|
matterMessage.Channel = channel
|
|
matterMessage.UserName = nick
|
|
matterMessage.Type = ""
|
|
matterMessage.Text = message
|
|
err := b.mh.Send(matterMessage)
|
|
if err != nil {
|
|
flog.Info(err)
|
|
return "", err
|
|
}
|
|
return "", nil
|
|
}
|
|
schannel, err := b.getChannelByName(channel)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
np := slack.NewPostMessageParameters()
|
|
if b.Config.PrefixMessagesWithNick {
|
|
np.AsUser = true
|
|
}
|
|
np.Username = nick
|
|
np.IconURL = config.GetIconURL(&msg, b.Config)
|
|
if msg.Avatar != "" {
|
|
np.IconURL = msg.Avatar
|
|
}
|
|
np.Attachments = append(np.Attachments, slack.Attachment{CallbackID: "matterbridge"})
|
|
// replace mentions
|
|
np.LinkNames = 1
|
|
|
|
if msg.Event == config.EVENT_MSG_DELETE {
|
|
// some protocols echo deletes, but with empty ID
|
|
if msg.ID == "" {
|
|
return "", nil
|
|
}
|
|
// we get a "slack <ID>", split it
|
|
ts := strings.Fields(msg.ID)
|
|
b.sc.DeleteMessage(schannel.ID, ts[1])
|
|
return "", nil
|
|
}
|
|
// if we have no ID it means we're creating a new message, not updating an existing one
|
|
if msg.ID != "" {
|
|
ts := strings.Fields(msg.ID)
|
|
b.sc.UpdateMessage(schannel.ID, ts[1], message)
|
|
return "", nil
|
|
}
|
|
_, id, err := b.sc.PostMessage(schannel.ID, message, np)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return "slack " + id, nil
|
|
}
|
|
|
|
func (b *Bslack) getAvatar(user string) string {
|
|
var avatar string
|
|
if b.Users != nil {
|
|
for _, u := range b.Users {
|
|
if user == u.Name {
|
|
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() {
|
|
mchan := make(chan *MMMessage)
|
|
if b.Config.WebhookBindAddress != "" {
|
|
flog.Debugf("Choosing webhooks based receiving")
|
|
go b.handleMatterHook(mchan)
|
|
} else {
|
|
flog.Debugf("Choosing token based receiving")
|
|
go b.handleSlackClient(mchan)
|
|
}
|
|
time.Sleep(time.Second)
|
|
flog.Debug("Start listening for Slack messages")
|
|
for message := range mchan {
|
|
// do not send messages from ourself
|
|
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" && message.Username == b.si.User.Name {
|
|
continue
|
|
}
|
|
if (message.Text == "" || message.Username == "") && message.Raw.SubType != "message_deleted" {
|
|
continue
|
|
}
|
|
text := message.Text
|
|
text = b.replaceURL(text)
|
|
text = html.UnescapeString(text)
|
|
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
|
|
msg := config.Message{Text: text, Username: message.Username, Channel: message.Channel, Account: b.Account, Avatar: b.getAvatar(message.Username), UserID: message.UserID, ID: "slack " + message.Raw.Timestamp}
|
|
if message.Raw.SubType == "me_message" {
|
|
msg.Event = config.EVENT_USER_ACTION
|
|
}
|
|
if message.Raw.SubType == "channel_leave" || message.Raw.SubType == "channel_join" {
|
|
msg.Username = "system"
|
|
msg.Event = config.EVENT_JOIN_LEAVE
|
|
}
|
|
// edited messages have a submessage, use this timestamp
|
|
if message.Raw.SubMessage != nil {
|
|
msg.ID = "slack " + message.Raw.SubMessage.Timestamp
|
|
}
|
|
if message.Raw.SubType == "message_deleted" {
|
|
msg.Text = config.EVENT_MSG_DELETE
|
|
msg.Event = config.EVENT_MSG_DELETE
|
|
msg.ID = "slack " + message.Raw.DeletedTimestamp
|
|
}
|
|
flog.Debugf("Message is %#v", msg)
|
|
b.Remote <- msg
|
|
}
|
|
}
|
|
|
|
func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
|
|
for msg := range b.rtm.IncomingEvents {
|
|
switch ev := msg.Data.(type) {
|
|
case *slack.MessageEvent:
|
|
flog.Debugf("Receiving from slackclient %#v", ev)
|
|
if len(ev.Attachments) > 0 {
|
|
// skip messages we made ourselves
|
|
if ev.Attachments[0].CallbackID == "matterbridge" {
|
|
continue
|
|
}
|
|
}
|
|
if !b.Config.EditDisable && ev.SubMessage != nil && ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp {
|
|
flog.Debugf("SubMessage %#v", ev.SubMessage)
|
|
ev.User = ev.SubMessage.User
|
|
ev.Text = ev.SubMessage.Text + b.Config.EditSuffix
|
|
}
|
|
// use our own func because rtm.GetChannelInfo doesn't work for private channels
|
|
channel, err := b.getChannelByID(ev.Channel)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
m := &MMMessage{}
|
|
if ev.BotID == "" && ev.SubType != "message_deleted" {
|
|
user, err := b.rtm.GetUserInfo(ev.User)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
m.UserID = user.ID
|
|
m.Username = user.Name
|
|
}
|
|
m.Channel = channel.Name
|
|
m.Text = ev.Text
|
|
if m.Text == "" {
|
|
for _, attach := range ev.Attachments {
|
|
if attach.Text != "" {
|
|
m.Text = attach.Text
|
|
} else {
|
|
m.Text = attach.Fallback
|
|
}
|
|
}
|
|
}
|
|
m.Raw = ev
|
|
m.Text = b.replaceMention(m.Text)
|
|
// when using webhookURL we can't check if it's our webhook or not for now
|
|
if ev.BotID != "" && b.Config.WebhookURL == "" {
|
|
bot, err := b.rtm.GetBotInfo(ev.BotID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if bot.Name != "" {
|
|
m.Username = bot.Name
|
|
m.UserID = bot.ID
|
|
}
|
|
}
|
|
mchan <- m
|
|
case *slack.OutgoingErrorEvent:
|
|
flog.Debugf("%#v", ev.Error())
|
|
case *slack.ChannelJoinedEvent:
|
|
b.Users, _ = b.sc.GetUsers()
|
|
case *slack.ConnectedEvent:
|
|
b.channels = ev.Info.Channels
|
|
b.si = ev.Info
|
|
b.Users, _ = b.sc.GetUsers()
|
|
// add private channels
|
|
groups, _ := b.sc.GetGroups(true)
|
|
for _, g := range groups {
|
|
channel := new(slack.Channel)
|
|
channel.ID = g.ID
|
|
channel.Name = g.Name
|
|
b.channels = append(b.channels, *channel)
|
|
}
|
|
case *slack.InvalidAuthEvent:
|
|
flog.Fatalf("Invalid Token %#v", ev)
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *Bslack) handleMatterHook(mchan chan *MMMessage) {
|
|
for {
|
|
message := b.mh.Receive()
|
|
flog.Debugf("receiving from matterhook (slack) %#v", message)
|
|
m := &MMMessage{}
|
|
m.Username = message.UserName
|
|
m.Text = message.Text
|
|
m.Text = b.replaceMention(m.Text)
|
|
m.Channel = message.ChannelName
|
|
if m.Username == "slackbot" {
|
|
continue
|
|
}
|
|
mchan <- m
|
|
}
|
|
}
|
|
|
|
func (b *Bslack) userName(id string) string {
|
|
for _, u := range b.Users {
|
|
if u.ID == id {
|
|
return u.Name
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (b *Bslack) replaceURL(text string) string {
|
|
results := regexp.MustCompile(`<(.*?)\|.*?>`).FindAllStringSubmatch(text, -1)
|
|
for _, r := range results {
|
|
text = strings.Replace(text, r[0], r[1], -1)
|
|
}
|
|
return text
|
|
}
|