summaryrefslogtreecommitdiff
path: root/teleirc/matterbridge/matterclient/matterclient.go
diff options
context:
space:
mode:
Diffstat (limited to 'teleirc/matterbridge/matterclient/matterclient.go')
-rw-r--r--teleirc/matterbridge/matterclient/matterclient.go294
1 files changed, 294 insertions, 0 deletions
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)
+ }
+}