summaryrefslogtreecommitdiff
path: root/teleirc/matterbridge/matterclient
diff options
context:
space:
mode:
Diffstat (limited to 'teleirc/matterbridge/matterclient')
-rw-r--r--teleirc/matterbridge/matterclient/channels.go226
-rw-r--r--teleirc/matterbridge/matterclient/helpers.go297
-rw-r--r--teleirc/matterbridge/matterclient/matterclient.go294
-rw-r--r--teleirc/matterbridge/matterclient/messages.go207
-rw-r--r--teleirc/matterbridge/matterclient/users.go165
5 files changed, 1189 insertions, 0 deletions
diff --git a/teleirc/matterbridge/matterclient/channels.go b/teleirc/matterbridge/matterclient/channels.go
new file mode 100644
index 0000000..13d3277
--- /dev/null
+++ b/teleirc/matterbridge/matterclient/channels.go
@@ -0,0 +1,226 @@
+package matterclient
+
+import (
+ "errors"
+ "strings"
+
+ "github.com/mattermost/mattermost-server/v5/model"
+)
+
+// 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...)
+ for _, t := range m.OtherTeams {
+ if t.Id != m.Team.Id {
+ channels = append(channels, t.Channels...)
+ }
+ }
+ return channels
+}
+
+func (m *MMClient) GetChannelHeader(channelId string) string { //nolint:golint
+ m.RLock()
+ defer m.RUnlock()
+ for _, t := range m.OtherTeams {
+ for _, channel := range append(t.Channels, t.MoreChannels...) {
+ if channel.Id == channelId {
+ return channel.Header
+ }
+
+ }
+ }
+ return ""
+}
+
+func getNormalisedName(channel *model.Channel) string {
+ if channel.Type == model.CHANNEL_GROUP {
+ // (deprecated in favor of ReplaceAll in go 1.12)
+ res := strings.Replace(channel.DisplayName, ", ", "-", -1) //nolint: gocritic
+ res = strings.Replace(res, " ", "_", -1) //nolint: gocritic
+ return res
+ }
+ return channel.Name
+}
+
+func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:golint
+ m.RLock()
+ defer m.RUnlock()
+ if teamId != "" {
+ return m.getChannelIdTeam(name, teamId)
+ }
+
+ for _, t := range m.OtherTeams {
+ for _, channel := range append(t.Channels, t.MoreChannels...) {
+ if getNormalisedName(channel) == name {
+ return channel.Id
+ }
+ }
+ }
+ return ""
+}
+
+func (m *MMClient) getChannelIdTeam(name string, teamId string) string { //nolint:golint
+ for _, t := range m.OtherTeams {
+ if t.Id == teamId {
+ for _, channel := range append(t.Channels, t.MoreChannels...) {
+ if getNormalisedName(channel) == name {
+ return channel.Id
+ }
+ }
+ }
+ }
+ return ""
+}
+
+func (m *MMClient) GetChannelName(channelId string) string { //nolint:golint
+ m.RLock()
+ defer m.RUnlock()
+ for _, t := range m.OtherTeams {
+ if t == nil {
+ continue
+ }
+ for _, channel := range append(t.Channels, t.MoreChannels...) {
+ if channel.Id == channelId {
+ return getNormalisedName(channel)
+ }
+ }
+ }
+ return ""
+}
+
+func (m *MMClient) GetChannelTeamId(id string) string { //nolint:golint
+ m.RLock()
+ defer m.RUnlock()
+ for _, t := range append(m.OtherTeams, m.Team) {
+ for _, channel := range append(t.Channels, t.MoreChannels...) {
+ if channel.Id == id {
+ return channel.TeamId
+ }
+ }
+ }
+ return ""
+}
+
+func (m *MMClient) GetLastViewedAt(channelId string) int64 { //nolint:golint
+ m.RLock()
+ defer m.RUnlock()
+ res, resp := m.Client.GetChannelMember(channelId, m.User.Id, "")
+ if resp.Error != nil {
+ return model.GetMillis()
+ }
+ return res.LastViewedAt
+}
+
+// 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...)
+ }
+ return channels
+}
+
+// GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId).
+func (m *MMClient) GetTeamFromChannel(channelId string) string { //nolint:golint
+ m.RLock()
+ defer m.RUnlock()
+ var channels []*model.Channel
+ for _, t := range m.OtherTeams {
+ channels = append(channels, t.Channels...)
+ if t.MoreChannels != nil {
+ channels = append(channels, t.MoreChannels...)
+ }
+ for _, c := range channels {
+ if c.Id == channelId {
+ if c.Type == model.CHANNEL_GROUP {
+ return "G"
+ }
+ return t.Id
+ }
+ }
+ channels = nil
+ }
+ return ""
+}
+
+func (m *MMClient) JoinChannel(channelId string) error { //nolint:golint
+ m.RLock()
+ defer m.RUnlock()
+ for _, c := range m.Team.Channels {
+ if c.Id == channelId {
+ m.logger.Debug("Not joining ", channelId, " already joined.")
+ return nil
+ }
+ }
+ m.logger.Debug("Joining ", channelId)
+ _, resp := m.Client.AddChannelMember(channelId, m.User.Id)
+ if resp.Error != nil {
+ return resp.Error
+ }
+ return nil
+}
+
+func (m *MMClient) UpdateChannelsTeam(teamID string) error {
+ mmchannels, resp := m.Client.GetChannelsForTeamForUser(teamID, m.User.Id, false, "")
+ if resp.Error != nil {
+ return errors.New(resp.Error.DetailedError)
+ }
+ for idx, t := range m.OtherTeams {
+ if t.Id == teamID {
+ m.Lock()
+ m.OtherTeams[idx].Channels = mmchannels
+ m.Unlock()
+ }
+ }
+
+ mmchannels, resp = m.Client.GetPublicChannelsForTeam(teamID, 0, 5000, "")
+ if resp.Error != nil {
+ return errors.New(resp.Error.DetailedError)
+ }
+ for idx, t := range m.OtherTeams {
+ if t.Id == teamID {
+ m.Lock()
+ m.OtherTeams[idx].MoreChannels = mmchannels
+ m.Unlock()
+ }
+ }
+ return nil
+}
+
+func (m *MMClient) UpdateChannels() error {
+ if err := m.UpdateChannelsTeam(m.Team.Id); err != nil {
+ return err
+ }
+ for _, t := range m.OtherTeams {
+ if err := m.UpdateChannelsTeam(t.Id); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (m *MMClient) UpdateChannelHeader(channelId string, header string) { //nolint:golint
+ channel := &model.Channel{Id: channelId, Header: header}
+ m.logger.Debugf("updating channelheader %#v, %#v", channelId, header)
+ _, resp := m.Client.UpdateChannel(channel)
+ if resp.Error != nil {
+ m.logger.Error(resp.Error)
+ }
+}
+
+func (m *MMClient) UpdateLastViewed(channelId string) error { //nolint:golint
+ m.logger.Debugf("posting lastview %#v", channelId)
+ view := &model.ChannelView{ChannelId: channelId}
+ _, resp := m.Client.ViewChannel(m.User.Id, view)
+ if resp.Error != nil {
+ m.logger.Errorf("ChannelView update for %s failed: %s", channelId, resp.Error)
+ return resp.Error
+ }
+ return nil
+}
diff --git a/teleirc/matterbridge/matterclient/helpers.go b/teleirc/matterbridge/matterclient/helpers.go
new file mode 100644
index 0000000..99743af
--- /dev/null
+++ b/teleirc/matterbridge/matterclient/helpers.go
@@ -0,0 +1,297 @@
+package matterclient
+
+import (
+ "crypto/md5" //nolint:gosec
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/cookiejar"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/gorilla/websocket"
+ "github.com/jpillora/backoff"
+ "github.com/mattermost/mattermost-server/v5/model"
+)
+
+func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error {
+ var resp *model.Response
+ var appErr *model.AppError
+ var logmsg = "trying login"
+ var err error
+ for {
+ m.logger.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
+ if m.Credentials.Token != "" {
+ resp, err = m.doLoginToken()
+ if err != nil {
+ return err
+ }
+ } else {
+ m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
+ }
+ appErr = resp.Error
+ if appErr != nil {
+ d := b.Duration()
+ m.logger.Debug(appErr.DetailedError)
+ if firstConnection {
+ if appErr.Message == "" {
+ return errors.New(appErr.DetailedError)
+ }
+ return errors.New(appErr.Message)
+ }
+ m.logger.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
+ time.Sleep(d)
+ logmsg = "retrying login"
+ continue
+ }
+ break
+ }
+ // reset timer
+ b.Reset()
+ return nil
+}
+
+func (m *MMClient) doLoginToken() (*model.Response, error) {
+ var resp *model.Response
+ var logmsg = "trying login"
+ m.Client.AuthType = model.HEADER_BEARER
+ m.Client.AuthToken = m.Credentials.Token
+ if m.Credentials.CookieToken {
+ m.logger.Debugf(logmsg + " with cookie (MMAUTH) token")
+ m.Client.HttpClient.Jar = m.createCookieJar(m.Credentials.Token)
+ } else {
+ m.logger.Debugf(logmsg + " with personal token")
+ }
+ m.User, resp = m.Client.GetMe("")
+ if resp.Error != nil {
+ return resp, resp.Error
+ }
+ if m.User == nil {
+ m.logger.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
+ return resp, errors.New("invalid token")
+ }
+ return resp, nil
+}
+
+func (m *MMClient) handleLoginToken() error {
+ switch {
+ case strings.Contains(m.Credentials.Pass, 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.Credentials.Token = token[1]
+ m.Credentials.CookieToken = true
+ case strings.Contains(m.Credentials.Pass, "token="):
+ token := strings.Split(m.Credentials.Pass, "token=")
+ if len(token) != 2 {
+ return errors.New("incorrect personal token. valid input is token=yourtoken")
+ }
+ m.Credentials.Token = token[1]
+ }
+ return nil
+}
+
+func (m *MMClient) initClient(firstConnection bool, b *backoff.Backoff) error {
+ uriScheme := "https://"
+ if m.NoTLS {
+ uriScheme = "http://"
+ }
+ // login to mattermost
+ m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server)
+ m.Client.HttpClient.Transport = &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec
+ Proxy: http.ProxyFromEnvironment,
+ }
+ m.Client.HttpClient.Timeout = time.Second * 10
+
+ // handle MMAUTHTOKEN and personal token
+ if err := m.handleLoginToken(); err != nil {
+ return err
+ }
+
+ // check if server alive, retry until
+ if err := m.serverAlive(firstConnection, b); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// initialize user and teams
+func (m *MMClient) initUser() error {
+ m.Lock()
+ defer m.Unlock()
+ // we only load all team data on initial login.
+ // all other updates are for channels from our (primary) team only.
+ //m.logger.Debug("initUser(): loading all team data")
+ teams, resp := m.Client.GetTeamsForUser(m.User.Id, "")
+ if resp.Error != nil {
+ return resp.Error
+ }
+ for _, team := range teams {
+ idx := 0
+ max := 200
+ usermap := make(map[string]*model.User)
+ mmusers, resp := m.Client.GetUsersInTeam(team.Id, idx, max, "")
+ if resp.Error != nil {
+ return errors.New(resp.Error.DetailedError)
+ }
+ for len(mmusers) > 0 {
+ for _, user := range mmusers {
+ usermap[user.Id] = user
+ }
+ mmusers, resp = m.Client.GetUsersInTeam(team.Id, idx, max, "")
+ if resp.Error != nil {
+ return errors.New(resp.Error.DetailedError)
+ }
+ idx++
+ time.Sleep(time.Millisecond * 200)
+ }
+ m.logger.Infof("found %d users in team %s", len(usermap), team.Name)
+
+ t := &Team{Team: team, Users: usermap, Id: team.Id}
+
+ mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, false, "")
+ if resp.Error != nil {
+ return resp.Error
+ }
+ t.Channels = mmchannels
+ mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "")
+ if resp.Error != nil {
+ return resp.Error
+ }
+ t.MoreChannels = mmchannels
+ m.OtherTeams = append(m.OtherTeams, t)
+ if team.Name == m.Credentials.Team {
+ m.Team = t
+ m.logger.Debugf("initUser(): found our team %s (id: %s)", team.Name, team.Id)
+ }
+ // add all users
+ for k, v := range t.Users {
+ m.Users[k] = v
+ }
+ }
+ return nil
+}
+
+func (m *MMClient) serverAlive(firstConnection bool, b *backoff.Backoff) error {
+ defer b.Reset()
+ for {
+ d := b.Duration()
+ // bogus call to get the serverversion
+ _, resp := m.Client.Logout()
+ if resp.Error != nil {
+ return fmt.Errorf("%#v", resp.Error.Error())
+ }
+ if firstConnection && !m.SkipVersionCheck && !supportedVersion(resp.ServerVersion) {
+ return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion)
+ }
+ if !m.SkipVersionCheck {
+ m.ServerVersion = resp.ServerVersion
+ if m.ServerVersion == "" {
+ m.logger.Debugf("Server not up yet, reconnecting in %s", d)
+ time.Sleep(d)
+ } else {
+ m.logger.Infof("Found version %s", m.ServerVersion)
+ return nil
+ }
+ } else {
+ return nil
+ }
+ }
+}
+
+func (m *MMClient) wsConnect() {
+ b := &backoff.Backoff{
+ Min: time.Second,
+ Max: 5 * time.Minute,
+ Jitter: true,
+ }
+
+ m.WsConnected = false
+ wsScheme := "wss://"
+ if m.NoTLS {
+ wsScheme = "ws://"
+ }
+
+ // setup websocket connection
+ wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V4 + "/websocket"
+ header := http.Header{}
+ header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
+
+ m.logger.Debugf("WsClient: making connection: %s", wsurl)
+ for {
+ wsDialer := &websocket.Dialer{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec
+ Proxy: http.ProxyFromEnvironment,
+ }
+ var err error
+ m.WsClient, _, err = wsDialer.Dial(wsurl, header)
+ if err != nil {
+ d := b.Duration()
+ m.logger.Debugf("WSS: %s, reconnecting in %s", err, d)
+ time.Sleep(d)
+ continue
+ }
+ break
+ }
+
+ m.logger.Debug("WsClient: connected")
+ m.WsSequence = 1
+ m.WsPingChan = make(chan *model.WebSocketResponse)
+ // only start to parse WS messages when login is completely done
+ m.WsConnected = true
+}
+
+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
+}
+
+func (m *MMClient) checkAlive() error {
+ // check if session still is valid
+ _, resp := m.Client.GetMe("")
+ if resp.Error != nil {
+ return resp.Error
+ }
+ m.logger.Debug("WS PING")
+ return m.sendWSRequest("ping", nil)
+}
+
+func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) error {
+ req := &model.WebSocketRequest{}
+ req.Seq = m.WsSequence
+ req.Action = action
+ req.Data = data
+ m.WsSequence++
+ m.logger.Debugf("sendWsRequest %#v", req)
+ return m.WsClient.WriteJSON(req)
+}
+
+func supportedVersion(version string) bool {
+ if strings.HasPrefix(version, "3.8.0") ||
+ strings.HasPrefix(version, "3.9.0") ||
+ strings.HasPrefix(version, "3.10.0") ||
+ strings.HasPrefix(version, "4.") ||
+ strings.HasPrefix(version, "5.") {
+ return true
+ }
+ return false
+}
+
+func digestString(s string) string {
+ return fmt.Sprintf("%x", md5.Sum([]byte(s))) //nolint:gosec
+}
diff --git a/teleirc/matterbridge/matterclient/matterclient.go b/teleirc/matterbridge/matterclient/matterclient.go
new file mode 100644
index 0000000..ffe88aa
--- /dev/null
+++ b/teleirc/matterbridge/matterclient/matterclient.go
@@ -0,0 +1,294 @@
+package matterclient
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gorilla/websocket"
+ lru "github.com/hashicorp/golang-lru"
+ "github.com/jpillora/backoff"
+ prefixed "github.com/matterbridge/logrus-prefixed-formatter"
+ "github.com/mattermost/mattermost-server/v5/model"
+ "github.com/sirupsen/logrus"
+)
+
+type Credentials struct {
+ Login string
+ Team string
+ Pass string
+ Token string
+ CookieToken bool
+ Server string
+ NoTLS bool
+ SkipTLSVerify bool
+ SkipVersionCheck bool
+}
+
+type Message struct {
+ Raw *model.WebSocketEvent
+ Post *model.Post
+ Team string
+ Channel string
+ Username string
+ Text string
+ Type string
+ UserID string
+}
+
+//nolint:golint
+type Team struct {
+ Team *model.Team
+ Id string
+ Channels []*model.Channel
+ MoreChannels []*model.Channel
+ Users map[string]*model.User
+}
+
+type MMClient struct {
+ sync.RWMutex
+ *Credentials
+
+ Team *Team
+ OtherTeams []*Team
+ Client *model.Client4
+ User *model.User
+ Users map[string]*model.User
+ MessageChan chan *Message
+ WsClient *websocket.Conn
+ WsQuit bool
+ WsAway bool
+ WsConnected bool
+ WsSequence int64
+ WsPingChan chan *model.WebSocketResponse
+ ServerVersion string
+ OnWsConnect func()
+
+ logger *logrus.Entry
+ rootLogger *logrus.Logger
+ lruCache *lru.Cache
+ allevents bool
+}
+
+// New will instantiate a new Matterclient with the specified login details without connecting.
+func New(login string, pass string, team string, server string) *MMClient {
+ rootLogger := logrus.New()
+ rootLogger.SetFormatter(&prefixed.TextFormatter{
+ PrefixPadding: 13,
+ DisableColors: true,
+ })
+
+ cred := &Credentials{
+ Login: login,
+ Pass: pass,
+ Team: team,
+ Server: server,
+ }
+
+ cache, _ := lru.New(500)
+ return &MMClient{
+ Credentials: cred,
+ MessageChan: make(chan *Message, 100),
+ Users: make(map[string]*model.User),
+ rootLogger: rootLogger,
+ lruCache: cache,
+ logger: rootLogger.WithFields(logrus.Fields{"prefix": "matterclient"}),
+ }
+}
+
+// SetDebugLog activates debugging logging on all Matterclient log output.
+func (m *MMClient) SetDebugLog() {
+ m.rootLogger.SetFormatter(&prefixed.TextFormatter{
+ PrefixPadding: 13,
+ DisableColors: true,
+ FullTimestamp: false,
+ ForceFormatting: true,
+ })
+}
+
+// SetLogLevel tries to parse the specified level and if successful sets
+// the log level accordingly. Accepted levels are: 'debug', 'info', 'warn',
+// 'error', 'fatal' and 'panic'.
+func (m *MMClient) SetLogLevel(level string) {
+ l, err := logrus.ParseLevel(level)
+ if err != nil {
+ m.logger.Warnf("Failed to parse specified log-level '%s': %#v", level, err)
+ } else {
+ m.rootLogger.SetLevel(l)
+ }
+}
+
+func (m *MMClient) EnableAllEvents() {
+ m.allevents = true
+}
+
+// Login tries to connect the client with the loging details with which it was initialized.
+func (m *MMClient) Login() error {
+ // check if this is a first connect or a reconnection
+ firstConnection := true
+ if m.WsConnected {
+ firstConnection = false
+ }
+ m.WsConnected = false
+ if m.WsQuit {
+ return nil
+ }
+ b := &backoff.Backoff{
+ Min: time.Second,
+ Max: 5 * time.Minute,
+ Jitter: true,
+ }
+
+ // do initialization setup
+ if err := m.initClient(firstConnection, b); err != nil {
+ return err
+ }
+
+ if err := m.doLogin(firstConnection, b); err != nil {
+ return err
+ }
+
+ if err := m.initUser(); err != nil {
+ return err
+ }
+
+ if m.Team == nil {
+ validTeamNames := make([]string, len(m.OtherTeams))
+ for i, t := range m.OtherTeams {
+ validTeamNames[i] = t.Team.Name
+ }
+ return fmt.Errorf("Team '%s' not found in %v", m.Credentials.Team, validTeamNames)
+ }
+
+ m.wsConnect()
+
+ return nil
+}
+
+// Logout disconnects the client from the chat server.
+func (m *MMClient) Logout() error {
+ m.logger.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()
+ if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
+ m.logger.Debug("Not invalidating session in logout, credential is a token")
+ return nil
+ }
+ _, resp := m.Client.Logout()
+ if resp.Error != nil {
+ return resp.Error
+ }
+ return nil
+}
+
+// WsReceiver implements the core loop that manages the connection to the chat server. In
+// case of a disconnect it will try to reconnect. A call to this method is blocking until
+// the 'WsQuite' field of the MMClient object is set to 'true'.
+func (m *MMClient) WsReceiver() {
+ for {
+ var rawMsg json.RawMessage
+ var err error
+
+ if m.WsQuit {
+ m.logger.Debug("exiting WsReceiver")
+ return
+ }
+
+ if !m.WsConnected {
+ time.Sleep(time.Millisecond * 100)
+ continue
+ }
+
+ if _, rawMsg, err = m.WsClient.ReadMessage(); err != nil {
+ m.logger.Error("error:", err)
+ // reconnect
+ m.wsConnect()
+ }
+
+ var event model.WebSocketEvent
+ if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
+ m.logger.Debugf("WsReceiver event: %#v", event)
+ msg := &Message{Raw: &event, Team: m.Credentials.Team}
+ m.parseMessage(msg)
+ // check if we didn't empty the message
+ if msg.Text != "" {
+ m.MessageChan <- msg
+ continue
+ }
+ // if we have file attached but the message is empty, also send it
+ if msg.Post != nil {
+ if msg.Text != "" || len(msg.Post.FileIds) > 0 || msg.Post.Type == "slack_attachment" {
+ m.MessageChan <- msg
+ continue
+ }
+ }
+ if m.allevents {
+ m.MessageChan <- msg
+ continue
+ }
+ switch msg.Raw.Event {
+ case model.WEBSOCKET_EVENT_USER_ADDED,
+ model.WEBSOCKET_EVENT_USER_REMOVED,
+ model.WEBSOCKET_EVENT_CHANNEL_CREATED,
+ model.WEBSOCKET_EVENT_CHANNEL_DELETED:
+ m.MessageChan <- msg
+ continue
+ }
+ }
+
+ var response model.WebSocketResponse
+ if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
+ m.logger.Debugf("WsReceiver response: %#v", response)
+ m.parseResponse(response)
+ }
+ }
+}
+
+// StatusLoop implements a ping-cycle that ensures that the connection to the chat servers
+// remains alive. In case of a disconnect it will try to reconnect. A call to this method
+// is blocking until the 'WsQuite' field of the MMClient object is set to 'true'.
+func (m *MMClient) StatusLoop() {
+ retries := 0
+ backoff := time.Second * 60
+ if m.OnWsConnect != nil {
+ m.OnWsConnect()
+ }
+ m.logger.Debug("StatusLoop:", m.OnWsConnect != nil)
+ for {
+ if m.WsQuit {
+ return
+ }
+ if m.WsConnected {
+ if err := m.checkAlive(); err != nil {
+ m.logger.Errorf("Connection is not alive: %#v", err)
+ }
+ select {
+ case <-m.WsPingChan:
+ m.logger.Debug("WS PONG received")
+ backoff = time.Second * 60
+ case <-time.After(time.Second * 5):
+ if retries > 3 {
+ m.logger.Debug("StatusLoop() timeout")
+ m.Logout()
+ m.WsQuit = false
+ err := m.Login()
+ if err != nil {
+ m.logger.Errorf("Login failed: %#v", err)
+ break
+ }
+ if m.OnWsConnect != nil {
+ m.OnWsConnect()
+ }
+ go m.WsReceiver()
+ } else {
+ retries++
+ backoff = time.Second * 5
+ }
+ }
+ }
+ time.Sleep(backoff)
+ }
+}
diff --git a/teleirc/matterbridge/matterclient/messages.go b/teleirc/matterbridge/matterclient/messages.go
new file mode 100644
index 0000000..81c3ecb
--- /dev/null
+++ b/teleirc/matterbridge/matterclient/messages.go
@@ -0,0 +1,207 @@
+package matterclient
+
+import (
+ "strings"
+
+ "github.com/mattermost/mattermost-server/v5/model"
+)
+
+func (m *MMClient) parseActionPost(rmsg *Message) {
+ // add post to cache, if it already exists don't relay this again.
+ // this should fix reposts
+ if ok, _ := m.lruCache.ContainsOrAdd(digestString(rmsg.Raw.Data["post"].(string)), true); ok && rmsg.Raw.Event != model.WEBSOCKET_EVENT_POST_DELETED {
+ m.logger.Debugf("message %#v in cache, not processing again", rmsg.Raw.Data["post"].(string))
+ rmsg.Text = ""
+ return
+ }
+ 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.logger.Infof("User '%v' is not known, ignoring message '%#v'",
+ data.UserId, data)
+ return
+ }
+ rmsg.Username = m.GetUserName(data.UserId)
+ rmsg.Channel = m.GetChannelName(data.ChannelId)
+ rmsg.UserID = data.UserId
+ rmsg.Type = data.Type
+ teamid, _ := rmsg.Raw.Data["team_id"].(string)
+ // edit messsages have no team_id for some reason
+ if teamid == "" {
+ // we can find the team_id from the channelid
+ teamid = m.GetChannelTeamId(data.ChannelId)
+ rmsg.Raw.Data["team_id"] = teamid
+ }
+ if teamid != "" {
+ rmsg.Team = m.GetTeamName(teamid)
+ }
+ // direct message
+ if rmsg.Raw.Data["channel_type"] == "D" {
+ rmsg.Channel = m.GetUser(data.UserId).Username
+ }
+ rmsg.Text = data.Message
+ rmsg.Post = data
+}
+
+func (m *MMClient) parseMessage(rmsg *Message) {
+ switch rmsg.Raw.Event {
+ case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED, model.WEBSOCKET_EVENT_POST_DELETED:
+ m.parseActionPost(rmsg)
+ case "user_updated":
+ user := rmsg.Raw.Data["user"].(map[string]interface{})
+ if _, ok := user["id"].(string); ok {
+ m.UpdateUser(user["id"].(string))
+ }
+ case "group_added":
+ if err := m.UpdateChannels(); err != nil {
+ m.logger.Errorf("failed to update channels: %#v", err)
+ }
+ /*
+ case model.ACTION_USER_REMOVED:
+ m.handleWsActionUserRemoved(&rmsg)
+ case model.ACTION_USER_ADDED:
+ m.handleWsActionUserAdded(&rmsg)
+ */
+ }
+}
+
+func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) {
+ if rmsg.Data != nil {
+ // ping reply
+ if rmsg.Data["text"].(string) == "pong" {
+ m.WsPingChan <- &rmsg
+ }
+ }
+}
+
+func (m *MMClient) DeleteMessage(postId string) error { //nolint:golint
+ _, resp := m.Client.DeletePost(postId)
+ if resp.Error != nil {
+ return resp.Error
+ }
+ return nil
+}
+
+func (m *MMClient) EditMessage(postId string, text string) (string, error) { //nolint:golint
+ post := &model.Post{Message: text, Id: postId}
+ res, resp := m.Client.UpdatePost(postId, post)
+ if resp.Error != nil {
+ return "", resp.Error
+ }
+ return res.Id, nil
+}
+
+func (m *MMClient) GetFileLinks(filenames []string) []string {
+ uriScheme := "https://"
+ if m.NoTLS {
+ uriScheme = "http://"
+ }
+
+ var output []string
+ for _, f := range filenames {
+ res, resp := m.Client.GetFileLink(f)
+ if resp.Error != nil {
+ // public links is probably disabled, create the link ourselves
+ output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V4+"/files/"+f)
+ continue
+ }
+ output = append(output, res)
+ }
+ return output
+}
+
+func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList { //nolint:golint
+ res, resp := m.Client.GetPostsForChannel(channelId, 0, limit, "", true)
+ if resp.Error != nil {
+ return nil
+ }
+ return res
+}
+
+func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList { //nolint:golint
+ res, resp := m.Client.GetPostsSince(channelId, time, true)
+ if resp.Error != nil {
+ return nil
+ }
+ return res
+}
+
+func (m *MMClient) GetPublicLink(filename string) string {
+ res, resp := m.Client.GetFileLink(filename)
+ if resp.Error != nil {
+ return ""
+ }
+ return res
+}
+
+func (m *MMClient) GetPublicLinks(filenames []string) []string {
+ var output []string
+ for _, f := range filenames {
+ res, resp := m.Client.GetFileLink(f)
+ if resp.Error != nil {
+ continue
+ }
+ output = append(output, res)
+ }
+ return output
+}
+
+func (m *MMClient) PostMessage(channelId string, text string, rootId string) (string, error) { //nolint:golint
+ post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId}
+ res, resp := m.Client.CreatePost(post)
+ if resp.Error != nil {
+ return "", resp.Error
+ }
+ return res.Id, nil
+}
+
+func (m *MMClient) PostMessageWithFiles(channelId string, text string, rootId string, fileIds []string) (string, error) { //nolint:golint
+ post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId, FileIds: fileIds}
+ res, resp := m.Client.CreatePost(post)
+ if resp.Error != nil {
+ return "", resp.Error
+ }
+ return res.Id, nil
+}
+
+func (m *MMClient) SearchPosts(query string) *model.PostList {
+ res, resp := m.Client.SearchPosts(m.Team.Id, query, false)
+ if resp.Error != nil {
+ return nil
+ }
+ return res
+}
+
+// SendDirectMessage sends a direct message to specified user
+func (m *MMClient) SendDirectMessage(toUserId string, msg string, rootId string) { //nolint:golint
+ m.SendDirectMessageProps(toUserId, msg, rootId, nil)
+}
+
+func (m *MMClient) SendDirectMessageProps(toUserId string, msg string, rootId string, props map[string]interface{}) { //nolint:golint
+ m.logger.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
+ // create DM channel (only happens on first message)
+ _, resp := m.Client.CreateDirectChannel(m.User.Id, toUserId)
+ if resp.Error != nil {
+ m.logger.Debugf("SendDirectMessage to %#v failed: %s", toUserId, resp.Error)
+ return
+ }
+ channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
+
+ // update our channels
+ if err := m.UpdateChannels(); err != nil {
+ m.logger.Errorf("failed to update channels: %#v", err)
+ }
+
+ // build & send the message
+ msg = strings.Replace(msg, "\r", "", -1)
+ post := &model.Post{ChannelId: m.GetChannelId(channelName, m.Team.Id), Message: msg, RootId: rootId, Props: props}
+ m.Client.CreatePost(post)
+}
+
+func (m *MMClient) UploadFile(data []byte, channelId string, filename string) (string, error) { //nolint:golint
+ f, resp := m.Client.UploadFile(data, channelId, filename)
+ if resp.Error != nil {
+ return "", resp.Error
+ }
+ return f.FileInfos[0].Id, nil
+}
diff --git a/teleirc/matterbridge/matterclient/users.go b/teleirc/matterbridge/matterclient/users.go
new file mode 100644
index 0000000..42c4162
--- /dev/null
+++ b/teleirc/matterbridge/matterclient/users.go
@@ -0,0 +1,165 @@
+package matterclient
+
+import (
+ "errors"
+ "time"
+
+ "github.com/mattermost/mattermost-server/v5/model"
+)
+
+func (m *MMClient) GetNickName(userId string) string { //nolint:golint
+ user := m.GetUser(userId)
+ if user != nil {
+ return user.Nickname
+ }
+ return ""
+}
+
+func (m *MMClient) GetStatus(userId string) string { //nolint:golint
+ res, resp := m.Client.GetUserStatus(userId, "")
+ if resp.Error != nil {
+ return ""
+ }
+ if res.Status == model.STATUS_AWAY {
+ return "away"
+ }
+ if res.Status == model.STATUS_ONLINE {
+ return "online"
+ }
+ return "offline"
+}
+
+func (m *MMClient) GetStatuses() map[string]string {
+ var ids []string
+ statuses := make(map[string]string)
+ for id := range m.Users {
+ ids = append(ids, id)
+ }
+ res, resp := m.Client.GetUsersStatusesByIds(ids)
+ if resp.Error != nil {
+ return statuses
+ }
+ for _, status := range res {
+ statuses[status.UserId] = "offline"
+ if status.Status == model.STATUS_AWAY {
+ statuses[status.UserId] = "away"
+ }
+ if status.Status == model.STATUS_ONLINE {
+ statuses[status.UserId] = "online"
+ }
+ }
+ return statuses
+}
+
+func (m *MMClient) GetTeamId() string { //nolint:golint
+ return m.Team.Id
+}
+
+// GetTeamName returns the name of the specified teamId
+func (m *MMClient) GetTeamName(teamId string) string { //nolint:golint
+ m.RLock()
+ defer m.RUnlock()
+ for _, t := range m.OtherTeams {
+ if t.Id == teamId {
+ return t.Team.Name
+ }
+ }
+ return ""
+}
+
+func (m *MMClient) GetUser(userId string) *model.User { //nolint:golint
+ m.Lock()
+ defer m.Unlock()
+ _, ok := m.Users[userId]
+ if !ok {
+ res, resp := m.Client.GetUser(userId, "")
+ if resp.Error != nil {
+ return nil
+ }
+ m.Users[userId] = res
+ }
+ return m.Users[userId]
+}
+
+func (m *MMClient) GetUserName(userId string) string { //nolint:golint
+ user := m.GetUser(userId)
+ if user != nil {
+ return user.Username
+ }
+ return ""
+}
+
+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) UpdateUsers() error {
+ idx := 0
+ max := 200
+ mmusers, resp := m.Client.GetUsers(idx, max, "")
+ if resp.Error != nil {
+ return errors.New(resp.Error.DetailedError)
+ }
+ for len(mmusers) > 0 {
+ m.Lock()
+ for _, user := range mmusers {
+ m.Users[user.Id] = user
+ }
+ m.Unlock()
+ mmusers, resp = m.Client.GetUsers(idx, max, "")
+ time.Sleep(time.Millisecond * 300)
+ if resp.Error != nil {
+ return errors.New(resp.Error.DetailedError)
+ }
+ idx++
+ }
+ return nil
+}
+
+func (m *MMClient) UpdateUserNick(nick string) error {
+ user := m.User
+ user.Nickname = nick
+ _, resp := m.Client.UpdateUser(user)
+ if resp.Error != nil {
+ return resp.Error
+ }
+ return nil
+}
+
+func (m *MMClient) UsernamesInChannel(channelId string) []string { //nolint:golint
+ res, resp := m.Client.GetChannelMembers(channelId, 0, 50000, "")
+ if resp.Error != nil {
+ m.logger.Errorf("UsernamesInChannel(%s) failed: %s", channelId, resp.Error)
+ return []string{}
+ }
+ allusers := m.GetUsers()
+ result := []string{}
+ for _, member := range *res {
+ result = append(result, allusers[member.UserId].Nickname)
+ }
+ return result
+}
+
+func (m *MMClient) UpdateStatus(userId string, status string) error { //nolint:golint
+ _, resp := m.Client.UpdateUserStatus(userId, &model.Status{Status: status})
+ if resp.Error != nil {
+ return resp.Error
+ }
+ return nil
+}
+
+func (m *MMClient) UpdateUser(userId string) { //nolint:golint
+ m.Lock()
+ defer m.Unlock()
+ res, resp := m.Client.GetUser(userId, "")
+ if resp.Error != nil {
+ return
+ }
+ m.Users[userId] = res
+}