From bd2a672c1436737b285c3a05f85b2757887a181c Mon Sep 17 00:00:00 2001
From: Wim <wim@42.be>
Date: Fri, 23 Feb 2018 23:07:23 +0100
Subject: [PATCH] Refactor mattermost

---
 bridge/helper/helper.go         |  20 ++
 bridge/mattermost/mattermost.go | 384 +++++++++++++++++---------------
 2 files changed, 227 insertions(+), 177 deletions(-)

diff --git a/bridge/helper/helper.go b/bridge/helper/helper.go
index 8bb105a4..907642c1 100644
--- a/bridge/helper/helper.go
+++ b/bridge/helper/helper.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"fmt"
 	"github.com/42wim/matterbridge/bridge/config"
+	log "github.com/sirupsen/logrus"
 	"io"
 	"net/http"
 	"time"
@@ -61,3 +62,22 @@ func GetAvatar(av map[string]string, userid string, general *config.Protocol) st
 	}
 	return ""
 }
+
+func HandleDownloadSize(flog *log.Entry, msg *config.Message, name string, size int64, general *config.Protocol) error {
+	flog.Debugf("Trying to download %#v with size %#v", name, size)
+	if int(size) > general.MediaDownloadSize {
+		msg.Event = config.EVENT_FILE_FAILURE_SIZE
+		msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{Name: name, Comment: msg.Text, Size: size})
+		return fmt.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, general.MediaDownloadSize)
+	}
+	return nil
+}
+
+func HandleDownloadData(flog *log.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) {
+	var avatar bool
+	flog.Debugf("Download OK %#v %#v", name, len(*data))
+	if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
+		avatar = true
+	}
+	msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data, URL: url, Comment: comment, Avatar: avatar})
+}
diff --git a/bridge/mattermost/mattermost.go b/bridge/mattermost/mattermost.go
index f6206493..887eed38 100644
--- a/bridge/mattermost/mattermost.go
+++ b/bridge/mattermost/mattermost.go
@@ -11,28 +11,9 @@ import (
 	"strings"
 )
 
-type MMhook struct {
-	mh *matterhook.Client
-}
-
-type MMapi struct {
-	mc    *matterclient.MMClient
-	mmMap map[string]string
-}
-
-type MMMessage struct {
-	Text     string
-	Channel  string
-	Username string
-	UserID   string
-	ID       string
-	Event    string
-	Extra    map[string][]interface{}
-}
-
 type Bmattermost struct {
-	MMhook
-	MMapi
+	mh     *matterhook.Client
+	mc     *matterclient.MMClient
 	TeamId string
 	*config.BridgeConfig
 	avatarMap map[string]string
@@ -47,7 +28,6 @@ func init() {
 
 func New(cfg *config.BridgeConfig) *Bmattermost {
 	b := &Bmattermost{BridgeConfig: cfg, avatarMap: make(map[string]string)}
-	b.mmMap = make(map[string]string)
 	return b
 }
 
@@ -143,135 +123,87 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
 
 func (b *Bmattermost) Send(msg config.Message) (string, error) {
 	flog.Debugf("Receiving %#v", msg)
+
+	// Make a action /me of the message
 	if msg.Event == config.EVENT_USER_ACTION {
 		msg.Text = "*" + msg.Text + "*"
 	}
-	nick := msg.Username
-	message := msg.Text
-	channel := msg.Channel
 
 	// map the file SHA to our user (caches the avatar)
 	if msg.Event == config.EVENT_AVATAR_DOWNLOAD {
-		fi := msg.Extra["file"][0].(config.FileInfo)
-		/* if we have a sha we have successfully uploaded the file to the media server,
-		so we can now cache the sha */
-		if fi.SHA != "" {
-			flog.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
-			b.avatarMap[msg.UserID] = fi.SHA
-		}
-		return "", nil
+		return b.cacheAvatar(&msg)
 	}
 
-	if b.Config.PrefixMessagesWithNick {
-		message = nick + message
-	}
+	// Use webhook to send the message
 	if b.Config.WebhookURL != "" {
-
-		if msg.Extra != nil {
-			for _, rmsg := range helper.HandleExtra(&msg, b.General) {
-				matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL, Channel: channel, UserName: rmsg.Username,
-					Text: rmsg.Text, Props: make(map[string]interface{})}
-				matterMessage.Props["matterbridge"] = true
-				b.mh.Send(matterMessage)
-			}
-			if len(msg.Extra["file"]) > 0 {
-				for _, f := range msg.Extra["file"] {
-					fi := f.(config.FileInfo)
-					if fi.URL != "" {
-						message += fi.URL
-					}
-				}
-			}
-		}
-
-		matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
-		matterMessage.IconURL = msg.Avatar
-		matterMessage.Channel = channel
-		matterMessage.UserName = nick
-		matterMessage.Type = ""
-		matterMessage.Text = message
-		matterMessage.Props = make(map[string]interface{})
-		matterMessage.Props["matterbridge"] = true
-		err := b.mh.Send(matterMessage)
-		if err != nil {
-			flog.Info(err)
-			return "", err
-		}
-		return "", nil
+		return b.sendWebhook(msg)
 	}
+
+	// Delete message
 	if msg.Event == config.EVENT_MSG_DELETE {
 		if msg.ID == "" {
 			return "", nil
 		}
 		return msg.ID, b.mc.DeleteMessage(msg.ID)
 	}
+
+	// Upload a file if it exists
 	if msg.Extra != nil {
 		for _, rmsg := range helper.HandleExtra(&msg, b.General) {
-			b.mc.PostMessage(b.mc.GetChannelId(channel, ""), rmsg.Username+rmsg.Text)
+			b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, ""), rmsg.Username+rmsg.Text)
 		}
 		if len(msg.Extra["file"]) > 0 {
-			var err error
-			var res, id string
-			for _, f := range msg.Extra["file"] {
-				fi := f.(config.FileInfo)
-				id, err = b.mc.UploadFile(*fi.Data, b.mc.GetChannelId(channel, ""), fi.Name)
-				if err != nil {
-					flog.Debugf("ERROR %#v", err)
-					return "", err
-				}
-				message = fi.Comment
-				if b.Config.PrefixMessagesWithNick {
-					message = nick + fi.Comment
-				}
-				res, err = b.mc.PostMessageWithFiles(b.mc.GetChannelId(channel, ""), message, []string{id})
-			}
-			return res, err
+			return b.handleUploadFile(&msg)
 		}
 	}
-	if msg.ID != "" {
-		return b.mc.EditMessage(msg.ID, message)
+
+	// Prepend nick if configured
+	if b.Config.PrefixMessagesWithNick {
+		msg.Text = msg.Username + msg.Text
 	}
-	return b.mc.PostMessage(b.mc.GetChannelId(channel, ""), message)
+
+	// Edit message if we have an ID
+	if msg.ID != "" {
+		return b.mc.EditMessage(msg.ID, msg.Text)
+	}
+
+	// Post normal message
+	return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, ""), msg.Text)
 }
 
 func (b *Bmattermost) handleMatter() {
-	mchan := make(chan *MMMessage)
+	messages := make(chan *config.Message)
 	if b.Config.WebhookBindAddress != "" {
 		flog.Debugf("Choosing webhooks based receiving")
-		go b.handleMatterHook(mchan)
+		go b.handleMatterHook(messages)
 	} else {
 		if b.Config.Token != "" {
 			flog.Debugf("Choosing token based receiving")
 		} else {
 			flog.Debugf("Choosing login/password based receiving")
 		}
-		go b.handleMatterClient(mchan)
+		go b.handleMatterClient(messages)
 	}
-	for message := range mchan {
-		avatar := helper.GetAvatar(b.avatarMap, message.UserID, b.General)
-		rmsg := config.Message{Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID, ID: message.ID, Event: message.Event, Extra: message.Extra, Avatar: avatar}
-		text, ok := b.replaceAction(message.Text)
+	var ok bool
+	for message := range messages {
+		message.Avatar = helper.GetAvatar(b.avatarMap, message.UserID, b.General)
+		message.Account = b.Account
+		message.Text, ok = b.replaceAction(message.Text)
 		if ok {
-			rmsg.Event = config.EVENT_USER_ACTION
+			message.Event = config.EVENT_USER_ACTION
 		}
-		rmsg.Text = text
 		flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
-		flog.Debugf("Message is %#v", rmsg)
-		b.Remote <- rmsg
+		flog.Debugf("Message is %#v", message)
+		b.Remote <- *message
 	}
 }
 
-func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
+func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
 	for message := range b.mc.MessageChan {
 		flog.Debugf("%#v", message.Raw.Data)
-		if message.Type == "system_join_leave" ||
-			message.Type == "system_join_channel" ||
-			message.Type == "system_leave_channel" {
-			flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
-			b.Remote <- config.Message{Username: "system", Text: message.Text, Channel: message.Channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
-			continue
-		}
-		if (message.Raw.Event == "post_edited") && b.Config.EditDisable {
+
+		if b.skipMessage(message) {
+			flog.Debugf("Skipped message: %#v", message)
 			continue
 		}
 
@@ -280,79 +212,47 @@ func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
 			b.handleDownloadAvatar(message.UserID, message.Channel)
 		}
 
-		m := &MMMessage{Extra: make(map[string][]interface{})}
+		flog.Debugf("Receiving from matterclient %#v", message)
 
+		rmsg := &config.Message{Username: message.Username, UserID: message.UserID, Channel: message.Channel, Text: message.Text, ID: message.Post.Id, Extra: make(map[string][]interface{})}
+
+		// handle mattermost post properties (override username and attachments)
 		props := message.Post.Props
 		if props != nil {
-			if _, ok := props["matterbridge"].(bool); ok {
-				flog.Debugf("sent by matterbridge, ignoring")
-				continue
-			}
 			if _, ok := props["override_username"].(string); ok {
-				message.Username = props["override_username"].(string)
+				rmsg.Username = props["override_username"].(string)
 			}
 			if _, ok := props["attachments"].([]interface{}); ok {
-				m.Extra["attachments"] = props["attachments"].([]interface{})
+				rmsg.Extra["attachments"] = props["attachments"].([]interface{})
 			}
 		}
-		// do not post our own messages back to irc
-		// only listen to message from our team
-		if (message.Raw.Event == "posted" || message.Raw.Event == "post_edited" || message.Raw.Event == "post_deleted") &&
-			b.mc.User.Username != message.Username && message.Raw.Data["team_id"].(string) == b.TeamId {
-			// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
-			if message.Post.HasReactions {
-				continue
-			}
-			flog.Debugf("Receiving from matterclient %#v", message)
-			m.UserID = message.UserID
-			m.Username = message.Username
-			m.Channel = message.Channel
-			m.Text = message.Text
-			m.ID = message.Post.Id
-			if message.Raw.Event == "post_edited" && !b.Config.EditDisable {
-				m.Text = message.Text + b.Config.EditSuffix
-			}
-			if message.Raw.Event == "post_deleted" {
-				m.Event = config.EVENT_MSG_DELETE
-			}
-			if len(message.Post.FileIds) > 0 {
-				for _, id := range message.Post.FileIds {
-					url, _ := b.mc.Client.GetFileLink(id)
-					finfo, resp := b.mc.Client.GetFileInfo(id)
-					if resp.Error != nil {
-						continue
-					}
-					flog.Debugf("trying to download %#v fileid %#v with size %#v", finfo.Name, finfo.Id, finfo.Size)
-					if int(finfo.Size) > b.General.MediaDownloadSize {
-						flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", finfo.Name, finfo.Size, b.General.MediaDownloadSize)
-						m.Event = config.EVENT_FILE_FAILURE_SIZE
-						m.Extra[m.Event] = append(m.Extra[m.Event], config.FileInfo{Name: finfo.Name, Comment: message.Text, Size: int64(finfo.Size)})
-						continue
-					}
-					data, resp := b.mc.Client.DownloadFile(id, true)
-					if resp.Error != nil {
-						flog.Errorf("download %s failed %#v", finfo.Name, resp.Error)
-						continue
-					}
-					flog.Debugf("download OK %#v %#v", finfo.Name, len(data))
-					m.Extra["file"] = append(m.Extra["file"], config.FileInfo{Name: finfo.Name, Data: &data, URL: url, Comment: message.Text})
+
+		// create a text for bridges that don't support native editing
+		if message.Raw.Event == "post_edited" && !b.Config.EditDisable {
+			rmsg.Text = message.Text + b.Config.EditSuffix
+		}
+
+		if message.Raw.Event == "post_deleted" {
+			rmsg.Event = config.EVENT_MSG_DELETE
+		}
+
+		if len(message.Post.FileIds) > 0 {
+			for _, id := range message.Post.FileIds {
+				err := b.handleDownloadFile(rmsg, id)
+				if err != nil {
+					flog.Errorf("download failed: %s", err)
 				}
 			}
-			mchan <- m
 		}
+		messages <- rmsg
 	}
 }
 
-func (b *Bmattermost) handleMatterHook(mchan chan *MMMessage) {
+func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
 	for {
 		message := b.mh.Receive()
 		flog.Debugf("Receiving from matterhook %#v", message)
-		m := &MMMessage{}
-		m.UserID = message.UserID
-		m.Username = message.UserName
-		m.Text = message.Text
-		m.Channel = message.ChannelName
-		mchan <- m
+		messages <- &config.Message{UserID: message.UserID, Username: message.UserName, Text: message.Text, Channel: message.ChannelName}
 	}
 }
 
@@ -362,8 +262,7 @@ func (b *Bmattermost) apiLogin() error {
 		password = "MMAUTHTOKEN=" + b.Config.Token
 	}
 
-	b.mc = matterclient.New(b.Config.Login, password,
-		b.Config.Team, b.Config.Server)
+	b.mc = matterclient.New(b.Config.Login, password, b.Config.Team, b.Config.Server)
 	if b.General.Debug {
 		b.mc.SetLogLevel("debug")
 	}
@@ -381,6 +280,7 @@ func (b *Bmattermost) apiLogin() error {
 	return nil
 }
 
+// replaceAction replace the message with the correct action (/me) code
 func (b *Bmattermost) replaceAction(text string) (string, bool) {
 	if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") {
 		return strings.Replace(text, "*", "", -1), true
@@ -388,26 +288,156 @@ func (b *Bmattermost) replaceAction(text string) (string, bool) {
 	return text, false
 }
 
+func (b *Bmattermost) cacheAvatar(msg *config.Message) (string, error) {
+	fi := msg.Extra["file"][0].(config.FileInfo)
+	/* if we have a sha we have successfully uploaded the file to the media server,
+	so we can now cache the sha */
+	if fi.SHA != "" {
+		flog.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
+		b.avatarMap[msg.UserID] = fi.SHA
+	}
+	return "", nil
+}
+
 // handleDownloadAvatar downloads the avatar of userid from channel
 // sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
 // logs an error message if it fails
 func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
-	var name string
-	msg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: userid, Event: config.EVENT_AVATAR_DOWNLOAD, Extra: make(map[string][]interface{})}
+	rmsg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: userid, Event: config.EVENT_AVATAR_DOWNLOAD, Extra: make(map[string][]interface{})}
 	if _, ok := b.avatarMap[userid]; !ok {
 		data, resp := b.mc.Client.GetProfileImage(userid, "")
 		if resp.Error != nil {
 			flog.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
+			return
 		}
-		if len(data) <= b.General.MediaDownloadSize {
-			name = userid + ".png"
-			flog.Debugf("download OK %#v %#v", name, len(data))
-			msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: &data, Avatar: true})
-			flog.Debugf("Sending avatar download message from %#v on %s to gateway", userid, b.Account)
-			flog.Debugf("Message is %#v", msg)
-			b.Remote <- msg
-		} else {
-			flog.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, len(data), b.General.MediaDownloadSize)
+		err := helper.HandleDownloadSize(flog, &rmsg, userid+".png", int64(len(data)), b.General)
+		if err != nil {
+			flog.Error(err)
+			return
 		}
+		helper.HandleDownloadData(flog, &rmsg, userid+".png", rmsg.Text, "", &data, b.General)
+		b.Remote <- rmsg
 	}
 }
+
+// handleDownloadFile handles file download
+func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
+	url, _ := b.mc.Client.GetFileLink(id)
+	finfo, resp := b.mc.Client.GetFileInfo(id)
+	if resp.Error != nil {
+		return resp.Error
+	}
+	err := helper.HandleDownloadSize(flog, rmsg, finfo.Name, int64(finfo.Size), b.General)
+	if err != nil {
+		return err
+	}
+	data, resp := b.mc.Client.DownloadFile(id, true)
+	if resp.Error != nil {
+		return resp.Error
+	}
+	helper.HandleDownloadData(flog, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
+	return nil
+}
+
+// handleUploadFile handles native upload of files
+func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
+	var err error
+	var res, id string
+	channelID := b.mc.GetChannelId(msg.Channel, "")
+	for _, f := range msg.Extra["file"] {
+		fi := f.(config.FileInfo)
+		id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name)
+		if err != nil {
+			return "", err
+		}
+		msg.Text = fi.Comment
+		if b.Config.PrefixMessagesWithNick {
+			msg.Text = msg.Username + msg.Text
+		}
+		res, err = b.mc.PostMessageWithFiles(channelID, msg.Text, []string{id})
+	}
+	return res, err
+}
+
+// sendWebhook uses the configured WebhookURL to send the message
+func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
+	if b.Config.PrefixMessagesWithNick {
+		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) {
+			matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL, Channel: rmsg.Channel, UserName: rmsg.Username, Text: rmsg.Text, Props: make(map[string]interface{})}
+			matterMessage.Props["matterbridge"] = true
+			b.mh.Send(matterMessage)
+		}
+
+		// 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
+				}
+			}
+		}
+	}
+
+	matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL, Channel: msg.Channel, UserName: msg.Username, Text: msg.Text, Props: make(map[string]interface{})}
+	if msg.Avatar != "" {
+		matterMessage.IconURL = msg.Avatar
+	}
+	matterMessage.Props["matterbridge"] = true
+	err := b.mh.Send(matterMessage)
+	if err != nil {
+		flog.Info(err)
+		return "", err
+	}
+	return "", nil
+}
+
+// skipMessages returns true if this message should not be handled
+func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
+	// Handle join/leave
+	if message.Type == "system_join_leave" ||
+		message.Type == "system_join_channel" ||
+		message.Type == "system_leave_channel" {
+		flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
+		b.Remote <- config.Message{Username: "system", Text: message.Text, Channel: message.Channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
+		return true
+	}
+
+	// Handle edited messages
+	if (message.Raw.Event == "post_edited") && b.Config.EditDisable {
+		return true
+	}
+
+	// Ignore messages sent from matterbridge
+	if message.Post.Props != nil {
+		if _, ok := message.Post.Props["matterbridge"].(bool); ok {
+			flog.Debugf("sent by matterbridge, ignoring")
+			return true
+		}
+	}
+
+	// Ignore messages sent from a user logged in as the bot
+	if b.mc.User.Username == message.Username {
+		return true
+	}
+
+	// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
+	if message.Post.HasReactions {
+		return true
+	}
+
+	// ignore messages from other teams than ours
+	if message.Raw.Data["team_id"].(string) != b.TeamId {
+		return true
+	}
+
+	// only handle posted, edited or deleted events
+	if !(message.Raw.Event == "posted" || message.Raw.Event == "post_edited" || message.Raw.Event == "post_deleted") {
+		return true
+	}
+	return false
+}