summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bridge/bridge.go437
-rw-r--r--bridge/config.go68
-rw-r--r--bridge/helper.go59
-rw-r--r--matterbridge.go9
-rw-r--r--matterclient/matterclient.go570
5 files changed, 1141 insertions, 2 deletions
diff --git a/bridge/bridge.go b/bridge/bridge.go
new file mode 100644
index 00000000..c2f48a33
--- /dev/null
+++ b/bridge/bridge.go
@@ -0,0 +1,437 @@
+package bridge
+
+import (
+ "crypto/tls"
+ "github.com/42wim/matterbridge/matterclient"
+ "github.com/42wim/matterbridge/matterhook"
+ log "github.com/Sirupsen/logrus"
+ "github.com/peterhellberg/giphy"
+ ircm "github.com/sorcix/irc"
+ "github.com/thoj/go-ircevent"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+)
+
+//type Bridge struct {
+type MMhook struct {
+ mh *matterhook.Client
+}
+
+type MMapi struct {
+ mc *matterclient.MMClient
+ mmMap map[string]string
+ mmIgnoreNicks []string
+}
+
+type MMirc struct {
+ i *irc.Connection
+ ircNick string
+ ircMap map[string]string
+ names map[string][]string
+ ircIgnoreNicks []string
+}
+
+type MMMessage struct {
+ Text string
+ Channel string
+ Username string
+}
+
+type Bridge struct {
+ MMhook
+ MMapi
+ MMirc
+ *Config
+ kind string
+}
+
+type FancyLog struct {
+ irc *log.Entry
+ mm *log.Entry
+}
+
+var flog FancyLog
+
+const Legacy = "legacy"
+
+func initFLog() {
+ flog.irc = log.WithFields(log.Fields{"module": "irc"})
+ flog.mm = log.WithFields(log.Fields{"module": "mattermost"})
+}
+
+func NewBridge(name string, config *Config, kind string) *Bridge {
+ initFLog()
+ b := &Bridge{}
+ b.Config = config
+ b.kind = kind
+ b.ircNick = b.Config.IRC.Nick
+ b.ircMap = make(map[string]string)
+ b.MMirc.names = make(map[string][]string)
+ b.ircIgnoreNicks = strings.Fields(b.Config.IRC.IgnoreNicks)
+ b.mmIgnoreNicks = strings.Fields(b.Config.Mattermost.IgnoreNicks)
+ if kind == Legacy {
+ if len(b.Config.Token) > 0 {
+ for _, val := range b.Config.Token {
+ b.ircMap[val.IRCChannel] = val.MMChannel
+ }
+ }
+
+ b.mh = matterhook.New(b.Config.Mattermost.URL,
+ matterhook.Config{Port: b.Config.Mattermost.Port, Token: b.Config.Mattermost.Token,
+ InsecureSkipVerify: b.Config.Mattermost.SkipTLSVerify,
+ BindAddress: b.Config.Mattermost.BindAddress})
+ } else {
+ b.mmMap = make(map[string]string)
+ if len(b.Config.Channel) > 0 {
+ for _, val := range b.Config.Channel {
+ b.ircMap[val.IRC] = val.Mattermost
+ b.mmMap[val.Mattermost] = val.IRC
+ }
+ }
+ b.mc = matterclient.New(b.Config.Mattermost.Login, b.Config.Mattermost.Password,
+ b.Config.Mattermost.Team, b.Config.Mattermost.Server)
+ b.mc.SkipTLSVerify = b.Config.Mattermost.SkipTLSVerify
+ b.mc.NoTLS = b.Config.Mattermost.NoTLS
+ flog.mm.Infof("Trying login %s (team: %s) on %s", b.Config.Mattermost.Login, b.Config.Mattermost.Team, b.Config.Mattermost.Server)
+ err := b.mc.Login()
+ if err != nil {
+ flog.mm.Fatal("Can not connect", err)
+ }
+ flog.mm.Info("Login ok")
+ b.mc.JoinChannel(b.Config.Mattermost.Channel)
+ if len(b.Config.Channel) > 0 {
+ for _, val := range b.Config.Channel {
+ b.mc.JoinChannel(val.Mattermost)
+ }
+ }
+ go b.mc.WsReceiver()
+ }
+ flog.irc.Info("Trying IRC connection")
+ b.i = b.createIRC(name)
+ flog.irc.Info("Connection succeeded")
+ go b.handleMatter()
+ return b
+}
+
+func (b *Bridge) createIRC(name string) *irc.Connection {
+ i := irc.IRC(b.Config.IRC.Nick, b.Config.IRC.Nick)
+ i.UseTLS = b.Config.IRC.UseTLS
+ i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.IRC.SkipTLSVerify}
+ if b.Config.IRC.Password != "" {
+ i.Password = b.Config.IRC.Password
+ }
+ i.AddCallback(ircm.RPL_WELCOME, b.handleNewConnection)
+ i.Connect(b.Config.IRC.Server + ":" + strconv.Itoa(b.Config.IRC.Port))
+ return i
+}
+
+func (b *Bridge) handleNewConnection(event *irc.Event) {
+ flog.irc.Info("Registering callbacks")
+ i := b.i
+ b.ircNick = event.Arguments[0]
+ i.AddCallback("PRIVMSG", b.handlePrivMsg)
+ i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
+ i.AddCallback(ircm.RPL_ENDOFNAMES, b.endNames)
+ i.AddCallback(ircm.RPL_NAMREPLY, b.storeNames)
+ i.AddCallback(ircm.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
+ i.AddCallback(ircm.NOTICE, b.handleNotice)
+ i.AddCallback(ircm.RPL_MYINFO, func(e *irc.Event) { flog.irc.Infof("%s: %s", e.Code, strings.Join(e.Arguments[1:], " ")) })
+ i.AddCallback("PING", func(e *irc.Event) {
+ i.SendRaw("PONG :" + e.Message())
+ flog.irc.Debugf("PING/PONG")
+ })
+ if b.Config.Mattermost.ShowJoinPart {
+ i.AddCallback("JOIN", b.handleJoinPart)
+ i.AddCallback("PART", b.handleJoinPart)
+ }
+ i.AddCallback("*", b.handleOther)
+ b.setupChannels()
+}
+
+func (b *Bridge) setupChannels() {
+ i := b.i
+ if b.Config.IRC.Channel != "" {
+ flog.irc.Infof("Joining %s as %s", b.Config.IRC.Channel, b.ircNick)
+ i.Join(b.Config.IRC.Channel)
+ }
+ if b.kind == Legacy {
+ for _, val := range b.Config.Token {
+ flog.irc.Infof("Joining %s as %s", val.IRCChannel, b.ircNick)
+ i.Join(val.IRCChannel)
+ }
+ } else {
+ for _, val := range b.Config.Channel {
+ flog.irc.Infof("Joining %s as %s", val.IRC, b.ircNick)
+ i.Join(val.IRC)
+ }
+ }
+}
+
+func (b *Bridge) handleIrcBotCommand(event *irc.Event) bool {
+ parts := strings.Fields(event.Message())
+ exp, _ := regexp.Compile("[:,]+$")
+ channel := event.Arguments[0]
+ command := ""
+ if len(parts) == 2 {
+ command = parts[1]
+ }
+ if exp.ReplaceAllString(parts[0], "") == b.ircNick {
+ switch command {
+ case "users":
+ usernames := b.mc.UsernamesInChannel(b.getMMChannel(channel))
+ sort.Strings(usernames)
+ b.i.Privmsg(channel, "Users on Mattermost: "+strings.Join(usernames, ", "))
+ default:
+ b.i.Privmsg(channel, "Valid commands are: [users, help]")
+ }
+ return true
+ }
+ return false
+}
+
+func (b *Bridge) ircNickFormat(nick string) string {
+ if nick == b.ircNick {
+ return nick
+ }
+ if b.Config.Mattermost.RemoteNickFormat == nil {
+ return "irc-" + nick
+ }
+ return strings.Replace(*b.Config.Mattermost.RemoteNickFormat, "{NICK}", nick, -1)
+}
+
+func (b *Bridge) handlePrivMsg(event *irc.Event) {
+ if b.ignoreMessage(event.Nick, event.Message(), "irc") {
+ return
+ }
+ if b.handleIrcBotCommand(event) {
+ return
+ }
+ msg := ""
+ if event.Code == "CTCP_ACTION" {
+ msg = event.Nick + " "
+ }
+ msg += event.Message()
+ b.Send(b.ircNickFormat(event.Nick), msg, b.getMMChannel(event.Arguments[0]))
+}
+
+func (b *Bridge) handleJoinPart(event *irc.Event) {
+ b.Send(b.ircNick, b.ircNickFormat(event.Nick)+" "+strings.ToLower(event.Code)+"s "+event.Message(), b.getMMChannel(event.Arguments[0]))
+}
+
+func (b *Bridge) handleNotice(event *irc.Event) {
+ if strings.Contains(event.Message(), "This nickname is registered") {
+ b.i.Privmsg(b.Config.IRC.NickServNick, "IDENTIFY "+b.Config.IRC.NickServPassword)
+ }
+}
+
+func (b *Bridge) nicksPerRow() int {
+ if b.Config.Mattermost.NicksPerRow < 1 {
+ return 4
+ }
+ return b.Config.Mattermost.NicksPerRow
+}
+
+func (b *Bridge) formatnicks(nicks []string, continued bool) string {
+ switch b.Config.Mattermost.NickFormatter {
+ case "table":
+ return tableformatter(nicks, b.nicksPerRow(), continued)
+ default:
+ return plainformatter(nicks, b.nicksPerRow())
+ }
+}
+
+func (b *Bridge) storeNames(event *irc.Event) {
+ channel := event.Arguments[2]
+ b.MMirc.names[channel] = append(
+ b.MMirc.names[channel],
+ strings.Split(strings.TrimSpace(event.Message()), " ")...)
+}
+
+func (b *Bridge) endNames(event *irc.Event) {
+ channel := event.Arguments[1]
+ sort.Strings(b.MMirc.names[channel])
+ maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
+ continued := false
+ for len(b.MMirc.names[channel]) > maxNamesPerPost {
+ b.Send(
+ b.ircNick,
+ b.formatnicks(b.MMirc.names[channel][0:maxNamesPerPost], continued),
+ b.getMMChannel(channel))
+ b.MMirc.names[channel] = b.MMirc.names[channel][maxNamesPerPost:]
+ continued = true
+ }
+ b.Send(b.ircNick, b.formatnicks(b.MMirc.names[channel], continued), b.getMMChannel(channel))
+ b.MMirc.names[channel] = nil
+}
+
+func (b *Bridge) handleTopicWhoTime(event *irc.Event) {
+ parts := strings.Split(event.Arguments[2], "!")
+ t, err := strconv.ParseInt(event.Arguments[3], 10, 64)
+ if err != nil {
+ flog.irc.Errorf("Invalid time stamp: %s", event.Arguments[3])
+ }
+ user := parts[0]
+ if len(parts) > 1 {
+ user += " [" + parts[1] + "]"
+ }
+ flog.irc.Infof("%s: Topic set by %s [%s]", event.Code, user, time.Unix(t, 0))
+}
+
+func (b *Bridge) handleOther(event *irc.Event) {
+ flog.irc.Debugf("%#v", event)
+}
+
+func (b *Bridge) Send(nick string, message string, channel string) error {
+ return b.SendType(nick, message, channel, "")
+}
+
+func (b *Bridge) SendType(nick string, message string, channel string, mtype string) error {
+ if b.Config.Mattermost.PrefixMessagesWithNick {
+ if IsMarkup(message) {
+ message = nick + "\n\n" + message
+ } else {
+ message = nick + " " + message
+ }
+ }
+ if b.kind == Legacy {
+ matterMessage := matterhook.OMessage{IconURL: b.Config.Mattermost.IconURL}
+ matterMessage.Channel = channel
+ matterMessage.UserName = nick
+ matterMessage.Type = mtype
+ matterMessage.Text = message
+ err := b.mh.Send(matterMessage)
+ if err != nil {
+ flog.mm.Info(err)
+ return err
+ }
+ return nil
+ }
+ flog.mm.Debug("->mattermost channel: ", channel, " ", message)
+ b.mc.PostMessage(channel, message)
+ return nil
+}
+
+func (b *Bridge) handleMatterHook(mchan chan *MMMessage) {
+ for {
+ message := b.mh.Receive()
+ m := &MMMessage{}
+ m.Username = message.UserName
+ m.Text = message.Text
+ m.Channel = message.Token
+ mchan <- m
+ }
+}
+
+func (b *Bridge) handleMatterClient(mchan chan *MMMessage) {
+ for message := range b.mc.MessageChan {
+ // do not post our own messages back to irc
+ if message.Raw.Action == "posted" && b.mc.User.Username != message.Username {
+ m := &MMMessage{}
+ m.Username = message.Username
+ m.Channel = message.Channel
+ m.Text = message.Text
+ flog.mm.Debugf("<-mattermost channel: %s %#v %#v", message.Channel, message.Post, message.Raw)
+ mchan <- m
+ }
+ }
+}
+
+func (b *Bridge) handleMatter() {
+ flog.mm.Infof("Choosing Mattermost connection type %s", b.kind)
+ mchan := make(chan *MMMessage)
+ if b.kind == Legacy {
+ go b.handleMatterHook(mchan)
+ } else {
+ go b.handleMatterClient(mchan)
+ }
+ flog.mm.Info("Start listening for Mattermost messages")
+ for message := range mchan {
+ var username string
+ if b.ignoreMessage(message.Username, message.Text, "mattermost") {
+ continue
+ }
+ username = message.Username + ": "
+ if b.Config.IRC.RemoteNickFormat != "" {
+ username = strings.Replace(b.Config.IRC.RemoteNickFormat, "{NICK}", message.Username, -1)
+ } else if b.Config.IRC.UseSlackCircumfix {
+ username = "<" + message.Username + "> "
+ }
+ cmds := strings.Fields(message.Text)
+ // empty message
+ if len(cmds) == 0 {
+ continue
+ }
+ cmd := cmds[0]
+ switch cmd {
+ case "!users":
+ flog.mm.Info("Received !users from ", message.Username)
+ b.i.SendRaw("NAMES " + b.getIRCChannel(message.Channel))
+ continue
+ case "!gif":
+ message.Text = b.giphyRandom(strings.Fields(strings.Replace(message.Text, "!gif ", "", 1)))
+ b.Send(b.ircNick, message.Text, b.getIRCChannel(message.Channel))
+ continue
+ }
+ texts := strings.Split(message.Text, "\n")
+ for _, text := range texts {
+ flog.mm.Debug("Sending message from " + message.Username + " to " + message.Channel)
+ b.i.Privmsg(b.getIRCChannel(message.Channel), username+text)
+ }
+ }
+}
+
+func (b *Bridge) giphyRandom(query []string) string {
+ g := giphy.DefaultClient
+ if b.Config.General.GiphyAPIKey != "" {
+ g.APIKey = b.Config.General.GiphyAPIKey
+ }
+ res, err := g.Random(query)
+ if err != nil {
+ return "error"
+ }
+ return res.Data.FixedHeightDownsampledURL
+}
+
+func (b *Bridge) getMMChannel(ircChannel string) string {
+ mmchannel, ok := b.ircMap[ircChannel]
+ if !ok {
+ mmchannel = b.Config.Mattermost.Channel
+ }
+ if b.kind == Legacy {
+ return mmchannel
+ }
+ return b.mc.GetChannelId(mmchannel, "")
+}
+
+func (b *Bridge) getIRCChannel(channel string) string {
+ if b.kind == Legacy {
+ ircchannel := b.Config.IRC.Channel
+ _, ok := b.Config.Token[channel]
+ if ok {
+ ircchannel = b.Config.Token[channel].IRCChannel
+ }
+ return ircchannel
+ }
+ ircchannel, ok := b.mmMap[channel]
+ if !ok {
+ ircchannel = b.Config.IRC.Channel
+ }
+ return ircchannel
+}
+
+func (b *Bridge) ignoreMessage(nick string, message string, protocol string) bool {
+ var ignoreNicks = b.mmIgnoreNicks
+ if protocol == "irc" {
+ ignoreNicks = b.ircIgnoreNicks
+ }
+ // should we discard messages ?
+ for _, entry := range ignoreNicks {
+ if nick == entry {
+ return true
+ }
+ }
+ return false
+}
diff --git a/bridge/config.go b/bridge/config.go
new file mode 100644
index 00000000..f31db98d
--- /dev/null
+++ b/bridge/config.go
@@ -0,0 +1,68 @@
+package bridge
+
+import (
+ "gopkg.in/gcfg.v1"
+ "io/ioutil"
+ "log"
+)
+
+type Config struct {
+ IRC struct {
+ UseTLS bool
+ SkipTLSVerify bool
+ Server string
+ Port int
+ Nick string
+ Password string
+ Channel string
+ UseSlackCircumfix bool
+ NickServNick string
+ NickServPassword string
+ RemoteNickFormat string
+ IgnoreNicks string
+ }
+ Mattermost struct {
+ URL string
+ Port int
+ ShowJoinPart bool
+ Token string
+ IconURL string
+ SkipTLSVerify bool
+ BindAddress string
+ Channel string
+ PrefixMessagesWithNick bool
+ NicksPerRow int
+ NickFormatter string
+ Server string
+ Team string
+ Login string
+ Password string
+ RemoteNickFormat *string
+ IgnoreNicks string
+ NoTLS bool
+ }
+ Token map[string]*struct {
+ IRCChannel string
+ MMChannel string
+ }
+ Channel map[string]*struct {
+ IRC string
+ Mattermost string
+ }
+ General struct {
+ GiphyAPIKey string
+ }
+}
+
+func NewConfig(cfgfile string) *Config {
+ var cfg Config
+ content, err := ioutil.ReadFile(cfgfile)
+ if err != nil {
+ log.Fatal(err)
+ }
+ err = gcfg.ReadStringInto(&cfg, string(content))
+ if err != nil {
+ log.Fatal("Failed to parse "+cfgfile+":", err)
+ }
+ return &cfg
+}
diff --git a/bridge/helper.go b/bridge/helper.go
new file mode 100644
index 00000000..7669ad57
--- /dev/null
+++ b/bridge/helper.go
@@ -0,0 +1,59 @@
+package bridge
+
+import (
+ "strings"
+)
+
+func tableformatter(nicks []string, nicksPerRow int, continued bool) string {
+ result := "|IRC users"
+ if continued {
+ result = "|(continued)"
+ }
+ for i := 0; i < 2; i++ {
+ for j := 1; j <= nicksPerRow && j <= len(nicks); j++ {
+ if i == 0 {
+ result += "|"
+ } else {
+ result += ":-|"
+ }
+ }
+ result += "\r\n|"
+ }
+ result += nicks[0] + "|"
+ for i := 1; i < len(nicks); i++ {
+ if i%nicksPerRow == 0 {
+ result += "\r\n|" + nicks[i] + "|"
+ } else {
+ result += nicks[i] + "|"
+ }
+ }
+ return result
+}
+
+func plainformatter(nicks []string, nicksPerRow int) string {
+ return strings.Join(nicks, ", ") + " currently on IRC"
+}
+
+func IsMarkup(message string) bool {
+ switch message[0] {
+ case '|':
+ fallthrough
+ case '#':
+ fallthrough
+ case '_':
+ fallthrough
+ case '*':
+ fallthrough
+ case '~':
+ fallthrough
+ case '-':
+ fallthrough
+ case ':':
+ fallthrough
+ case '>':
+ fallthrough
+ case '=':
+ return true
+ }
+ return false
+}
diff --git a/matterbridge.go b/matterbridge.go
index a40ec8a0..db0ebf00 100644
--- a/matterbridge.go
+++ b/matterbridge.go
@@ -3,7 +3,7 @@ package main
import (
"flag"
"fmt"
- "github.com/42wim/matterbridge-plus/bridge"
+ "github.com/42wim/matterbridge/bridge"
log "github.com/Sirupsen/logrus"
)
@@ -17,6 +17,7 @@ func main() {
flagConfig := flag.String("conf", "matterbridge.conf", "config file")
flagDebug := flag.Bool("debug", false, "enable debug")
flagVersion := flag.Bool("version", false, "show version")
+ flagPlus := flag.Bool("plus", false, "running using API instead of webhooks")
flag.Parse()
if *flagVersion {
fmt.Println("Version:", Version)
@@ -28,6 +29,10 @@ func main() {
log.SetLevel(log.DebugLevel)
}
fmt.Println("running version", Version)
- bridge.NewBridge("matterbot", bridge.NewConfig(*flagConfig), "legacy")
+ if *flagPlus {
+ bridge.NewBridge("matterbot", bridge.NewConfig(*flagConfig), "")
+ } else {
+ bridge.NewBridge("matterbot", bridge.NewConfig(*flagConfig), "legacy")
+ }
select {}
}
diff --git a/matterclient/matterclient.go b/matterclient/matterclient.go
new file mode 100644
index 00000000..b4bafcd1
--- /dev/null
+++ b/matterclient/matterclient.go
@@ -0,0 +1,570 @@
+package matterclient
+
+import (
+ "crypto/tls"
+ "errors"
+ "net/http"
+ "net/http/cookiejar"
+ "net/url"
+ "strings"
+ "sync"
+ "time"
+
+ log "github.com/Sirupsen/logrus"
+
+ "github.com/gorilla/websocket"
+ "github.com/jpillora/backoff"
+ "github.com/mattermost/platform/model"
+)
+
+type Credentials struct {
+ Login string
+ Team string
+ Pass string
+ Server string
+ NoTLS bool
+ SkipTLSVerify bool
+}
+
+type Message struct {
+ Raw *model.Message
+ Post *model.Post
+ Team string
+ Channel string
+ Username string
+ Text string
+}
+
+type Team struct {
+ Team *model.Team
+ Id string
+ Channels *model.ChannelList
+ MoreChannels *model.ChannelList
+ Users map[string]*model.User
+}
+
+type MMClient struct {
+ sync.RWMutex
+ *Credentials
+ Team *Team
+ OtherTeams []*Team
+ Client *model.Client
+ WsClient *websocket.Conn
+ WsQuit bool
+ WsAway bool
+ WsConnected bool
+ User *model.User
+ Users map[string]*model.User
+ MessageChan chan *Message
+ log *log.Entry
+}
+
+func New(login, pass, team, server string) *MMClient {
+ cred := &Credentials{Login: login, Pass: pass, Team: team, Server: server}
+ mmclient := &MMClient{Credentials: cred, MessageChan: make(chan *Message, 100), Users: make(map[string]*model.User)}
+ mmclient.log = log.WithFields(log.Fields{"module": "matterclient"})
+ log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
+ return mmclient
+}
+
+func (m *MMClient) SetLogLevel(level string) {
+ l, err := log.ParseLevel(level)
+ if err != nil {
+ log.SetLevel(log.InfoLevel)
+ return
+ }
+ log.SetLevel(l)
+}
+
+func (m *MMClient) Login() error {
+ m.WsConnected = false
+ if m.WsQuit {
+ return nil
+ }
+ b := &backoff.Backoff{
+ Min: time.Second,
+ Max: 5 * time.Minute,
+ Jitter: true,
+ }
+ uriScheme := "https://"
+ wsScheme := "wss://"
+ if m.NoTLS {
+ uriScheme = "http://"
+ wsScheme = "ws://"
+ }
+ // login to mattermost
+ m.Client = model.NewClient(uriScheme + m.Credentials.Server)
+ m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
+ var myinfo *model.Result
+ var appErr *model.AppError
+ var logmsg = "trying login"
+ for {
+ m.log.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
+ if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
+ m.log.Debugf(logmsg+" with %s", model.SESSION_COOKIE_TOKEN)
+ token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
+ if len(token) != 2 {
+ return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken")
+ }
+ m.Client.HttpClient.Jar = m.createCookieJar(token[1])
+ m.Client.MockSession(token[1])
+ myinfo, appErr = m.Client.GetMe("")
+ if appErr != nil {
+ return errors.New(appErr.DetailedError)
+ }
+ if myinfo.Data.(*model.User) == nil {
+ m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
+ return errors.New("invalid " + model.SESSION_COOKIE_TOKEN)
+ }
+ } else {
+ myinfo, appErr = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
+ }
+ if appErr != nil {
+ d := b.Duration()
+ m.log.Debug(appErr.DetailedError)
+ if !strings.Contains(appErr.DetailedError, "connection refused") &&
+ !strings.Contains(appErr.DetailedError, "invalid character") {
+ if appErr.Message == "" {
+ return errors.New(appErr.DetailedError)
+ }
+ return errors.New(appErr.Message)
+ }
+ m.log.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
+ time.Sleep(d)
+ logmsg = "retrying login"
+ continue
+ }
+ break
+ }
+ // reset timer
+ b.Reset()
+
+ err := m.initUser()
+ if err != nil {
+ return err
+ }
+
+ // set our team id as default route
+ m.Client.SetTeamId(m.Team.Id)
+ if m.Team == nil {
+ return errors.New("team not found")
+ }
+
+ // setup websocket connection
+ wsurl := wsScheme + m.Credentials.Server + "/api/v3/users/websocket"
+ header := http.Header{}
+ header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
+
+ m.log.Debug("WsClient: making connection")
+ for {
+ wsDialer := &websocket.Dialer{Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
+ m.WsClient, _, err = wsDialer.Dial(wsurl, header)
+ if err != nil {
+ d := b.Duration()
+ m.log.Debugf("WSS: %s, reconnecting in %s", err, d)
+ time.Sleep(d)
+ continue
+ }
+ break
+ }
+ b.Reset()
+
+ // only start to parse WS messages when login is completely done
+ m.WsConnected = true
+
+ return nil
+}
+
+func (m *MMClient) Logout() error {
+ m.log.Debugf("logout as %s (team: %s) on %s", m.Credentials.Login, m.Credentials.Team, m.Credentials.Server)
+ m.WsQuit = true
+ m.WsClient.Close()
+ m.WsClient.UnderlyingConn().Close()
+ m.WsClient = nil
+ _, err := m.Client.Logout()
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (m *MMClient) WsReceiver() {
+ var rmsg model.Message
+ for {
+ if m.WsQuit {
+ m.log.Debug("exiting WsReceiver")
+ return
+ }
+ if err := m.WsClient.ReadJSON(&rmsg); err != nil {
+ m.log.Error("error:", err)
+ // reconnect
+ m.Login()
+ }
+ // we're not fully logged in yet.
+ if !m.WsConnected {
+ continue
+ }
+ if rmsg.Action == "ping" {
+ m.handleWsPing()
+ continue
+ }
+ msg := &Message{Raw: &rmsg, Team: m.Credentials.Team}
+ m.parseMessage(msg)
+ m.MessageChan <- msg
+ }
+
+}
+
+func (m *MMClient) handleWsPing() {
+ m.log.Debug("Ws PING")
+ if !m.WsQuit && !m.WsAway {
+ m.log.Debug("Ws PONG")
+ m.WsClient.WriteMessage(websocket.PongMessage, []byte{})
+ }
+}
+
+func (m *MMClient) parseMessage(rmsg *Message) {
+ switch rmsg.Raw.Action {
+ case model.ACTION_POSTED:
+ m.parseActionPost(rmsg)
+ /*
+ case model.ACTION_USER_REMOVED:
+ m.handleWsActionUserRemoved(&rmsg)
+ case model.ACTION_USER_ADDED:
+ m.handleWsActionUserAdded(&rmsg)
+ */
+ }
+}
+
+func (m *MMClient) parseActionPost(rmsg *Message) {
+ data := model.PostFromJson(strings.NewReader(rmsg.Raw.Props["post"]))
+ // we don't have the user, refresh the userlist
+ if m.GetUser(data.UserId) == nil {
+ m.UpdateUsers()
+ }
+ rmsg.Username = m.GetUser(data.UserId).Username
+ rmsg.Channel = m.GetChannelName(data.ChannelId)
+ rmsg.Team = m.GetTeamName(rmsg.Raw.TeamId)
+ // direct message
+ if data.Type == "D" {
+ rmsg.Channel = m.GetUser(data.UserId).Username
+ }
+ rmsg.Text = data.Message
+ rmsg.Post = data
+ return
+}
+
+func (m *MMClient) UpdateUsers() error {
+ mmusers, _ := m.Client.GetProfilesForDirectMessageList(m.Team.Id)
+ m.Lock()
+ m.Users = mmusers.Data.(map[string]*model.User)
+ m.Unlock()
+ return nil
+}
+
+func (m *MMClient) UpdateChannels() error {
+ mmchannels, _ := m.Client.GetChannels("")
+ mmchannels2, _ := m.Client.GetMoreChannels("")
+ m.Lock()
+ m.Team.Channels = mmchannels.Data.(*model.ChannelList)
+ m.Team.MoreChannels = mmchannels2.Data.(*model.ChannelList)
+ m.Unlock()
+ return nil
+}
+
+func (m *MMClient) GetChannelName(channelId string) string {
+ m.RLock()
+ defer m.RUnlock()
+ for _, t := range m.OtherTeams {
+ for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
+ if channel.Id == channelId {
+ return channel.Name
+ }
+ }
+ }
+ return ""
+}
+
+func (m *MMClient) GetChannelId(name string, teamId string) string {
+ m.RLock()
+ defer m.RUnlock()
+ if teamId == "" {
+ teamId = m.Team.Id
+ }
+ for _, t := range m.OtherTeams {
+ if t.Id == teamId {
+ for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
+ if channel.Name == name {
+ return channel.Id
+ }
+ }
+ }
+ }
+ return ""
+}
+
+func (m *MMClient) GetChannelHeader(channelId string) string {
+ m.RLock()
+ defer m.RUnlock()
+ for _, t := range m.OtherTeams {
+ for _, channel := range append(t.Channels.Channels, t.MoreChannels.Channels...) {
+ if channel.Id == channelId {
+ return channel.Header
+ }
+
+ }
+ }
+ return ""
+}
+
+func (m *MMClient) PostMessage(channelId string, text string) {
+ post := &model.Post{ChannelId: channelId, Message: text}
+ m.Client.CreatePost(post)
+}
+
+func (m *MMClient) JoinChannel(channelId string) error {
+ m.RLock()
+ defer m.RUnlock()
+ for _, c := range m.Team.Channels.Channels {
+ if c.Id == channelId {
+ m.log.Debug("Not joining ", channelId, " already joined.")
+ return nil
+ }
+ }
+ m.log.Debug("Joining ", channelId)
+ _, err := m.Client.JoinChannel(channelId)
+ if err != nil {
+ return errors.New("failed to join")
+ }
+ return nil
+}
+
+func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList {
+ res, err := m.Client.GetPostsSince(channelId, time)
+ if err != nil {
+ return nil
+ }
+ return res.Data.(*model.PostList)
+}
+
+func (m *MMClient) SearchPosts(query string) *model.PostList {
+ res, err := m.Client.SearchPosts(query, false)
+ if err != nil {
+ return nil
+ }
+ return res.Data.(*model.PostList)
+}
+
+func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList {
+ res, err := m.Client.GetPosts(channelId, 0, limit, "")
+ if err != nil {
+ return nil
+ }
+ return res.Data.(*model.PostList)
+}
+
+func (m *MMClient) GetPublicLink(filename string) string {
+ res, err := m.Client.GetPublicLink(filename)
+ if err != nil {
+ return ""
+ }
+ return res.Data.(string)
+}
+
+func (m *MMClient) GetPublicLinks(filenames []string) []string {
+ var output []string
+ for _, f := range filenames {
+ res, err := m.Client.GetPublicLink(f)
+ if err != nil {
+ continue
+ }
+ output = append(output, res.Data.(string))
+ }
+ return output
+}
+
+func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
+ data := make(map[string]string)
+ data["channel_id"] = channelId
+ data["channel_header"] = header
+ m.log.Debugf("updating channelheader %#v, %#v", channelId, header)
+ _, err := m.Client.UpdateChannelHeader(data)
+ if err != nil {
+ log.Error(err)
+ }
+}
+
+func (m *MMClient) UpdateLastViewed(channelId string) {
+ m.log.Debugf("posting lastview %#v", channelId)
+ _, err := m.Client.UpdateLastViewedAt(channelId)
+ if err != nil {
+ m.log.Error(err)
+ }
+}
+
+func (m *MMClient) UsernamesInChannel(channelId string) []string {
+ ceiRes, err := m.Client.GetChannelExtraInfo(channelId, 5000, "")
+ if err != nil {
+ m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelId, err)
+ return []string{}
+ }
+ extra := ceiRes.Data.(*model.ChannelExtra)
+ result := []string{}
+ for _, member := range extra.Members {
+ result = append(result, member.Username)
+ }
+ return result
+}
+
+func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
+ var cookies []*http.Cookie
+ jar, _ := cookiejar.New(nil)
+ firstCookie := &http.Cookie{
+ Name: "MMAUTHTOKEN",
+ Value: token,
+ Path: "/",
+ Domain: m.Credentials.Server,
+ }
+ cookies = append(cookies, firstCookie)
+ cookieURL, _ := url.Parse("https://" + m.Credentials.Server)
+ jar.SetCookies(cookieURL, cookies)
+ return jar
+}
+
+// SendDirectMessage sends a direct message to specified user
+func (m *MMClient) SendDirectMessage(toUserId string, msg string) {
+ m.log.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
+ // create DM channel (only happens on first message)
+ _, err := m.Client.CreateDirectChannel(toUserId)
+ if err != nil {
+ m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, err)
+ }
+ channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
+
+ // update our channels
+ mmchannels, _ := m.Client.GetChannels("")
+ m.Lock()
+ m.Team.Channels = mmchannels.Data.(*model.ChannelList)
+ m.Unlock()
+
+ // build & send the message
+ msg = strings.Replace(msg, "\r", "", -1)
+ post := &model.Post{ChannelId: m.GetChannelId(channelName, ""), Message: msg}
+ m.Client.CreatePost(post)
+}
+
+// GetTeamName returns the name of the specified teamId
+func (m *MMClient) GetTeamName(teamId string) string {
+ m.RLock()
+ defer m.RUnlock()
+ for _, t := range m.OtherTeams {
+ if t.Id == teamId {
+ return t.Team.Name
+ }
+ }
+ return ""
+}
+
+// GetChannels returns all channels we're members off
+func (m *MMClient) GetChannels() []*model.Channel {
+ m.RLock()
+ defer m.RUnlock()
+ var channels []*model.Channel
+ // our primary team channels first
+ channels = append(channels, m.Team.Channels.Channels...)
+ for _, t := range m.OtherTeams {
+ if t.Id != m.Team.Id {
+ channels = append(channels, t.Channels.Channels...)
+ }
+ }
+ return channels
+}
+
+// GetMoreChannels returns existing channels where we're not a member off.
+func (m *MMClient) GetMoreChannels() []*model.Channel {
+ m.RLock()
+ defer m.RUnlock()
+ var channels []*model.Channel
+ for _, t := range m.OtherTeams {
+ channels = append(channels, t.MoreChannels.Channels...)
+ }
+ return channels
+}
+
+// GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId).
+func (m *MMClient) GetTeamFromChannel(channelId string) string {
+ m.RLock()
+ defer m.RUnlock()
+ var channels []*model.Channel
+ for _, t := range m.OtherTeams {
+ channels = append(channels, t.Channels.Channels...)
+ for _, c := range channels {
+ if c.Id == channelId {
+ return t.Id
+ }
+ }
+ }
+ return ""
+}
+
+func (m *MMClient) GetLastViewedAt(channelId string) int64 {
+ m.RLock()
+ defer m.RUnlock()
+ for _, t := range m.OtherTeams {
+ if _, ok := t.Channels.Members[channelId]; ok {
+ return t.Channels.Members[channelId].LastViewedAt
+ }
+ }
+ return 0
+}
+
+func (m *MMClient) GetUsers() map[string]*model.User {
+ users := make(map[string]*model.User)
+ m.RLock()
+ defer m.RUnlock()
+ for k, v := range m.Users {
+ users[k] = v
+ }
+ return users
+}
+
+func (m *MMClient) GetUser(userId string) *model.User {
+ m.RLock()
+ defer m.RUnlock()
+ return m.Users[userId]
+}
+
+// initialize user and teams
+func (m *MMClient) initUser() error {
+ m.Lock()
+ defer m.Unlock()
+ m.log.Debug("initUser()")
+ initLoad, err := m.Client.GetInitialLoad()
+ if err != nil {
+ return err
+ }
+ initData := initLoad.Data.(*model.InitialLoad)
+ m.User = initData.User
+ // we only load all team data on initial login.
+ // all other updates are for channels from our (primary) team only.
+ m.log.Debug("initUser(): loading all team data")
+ for _, v := range initData.Teams {
+ m.Client.SetTeamId(v.Id)
+ mmusers, _ := m.Client.GetProfiles(v.Id, "")
+ t := &Team{Team: v, Users: mmusers.Data.(map[string]*model.User), Id: v.Id}
+ mmchannels, _ := m.Client.GetChannels("")
+ t.Channels = mmchannels.Data.(*model.ChannelList)
+ mmchannels, _ = m.Client.GetMoreChannels("")
+ t.MoreChannels = mmchannels.Data.(*model.ChannelList)
+ m.OtherTeams = append(m.OtherTeams, t)
+ if v.Name == m.Credentials.Team {
+ m.Team = t
+ m.log.Debugf("initUser(): found our team %s (id: %s)", v.Name, v.Id)
+ }
+ // add all users
+ for k, v := range t.Users {
+ m.Users[k] = v
+ }
+ }
+ return nil
+}