aboutsummaryrefslogblamecommitdiff
path: root/connector/mattermost/mattermost.go
blob: 330026ad5bb5772f72490237b92f667f5a534080 (plain) (tree)
1
2
3
4
5
6
7
8






                  
                       


                                                       
                                        















                                                             
 


                                                                                    
































                                                        




                                                





                                                                             
                                         











































































                                                                                                                          








                                                                             




































                                                                                




                                                







                                                                
                             







                                             





                                                     
                                   




                                                          












                                                                                  


                                                   

                                                  



                                 











                                            
                                            

                                                       



























                                                                                      
                                                            



                                                                      



                         





















                                                                        

                                                                                   
                                                             
                                              
                  
                                                        

         









                                          























                                                                                       














                                                               








                                                                   














                                                                               
package mattermost

import (
	"fmt"
	_ "os"
	"strings"
	"time"
	"encoding/json"

	"github.com/mattermost/mattermost-server/model"
	"github.com/42wim/matterbridge/matterclient"
	log "github.com/sirupsen/logrus"

	. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
)

// User id format: nickname@server
// Room id format: room_name@team@server

type Mattermost struct {
	handler Handler

	server string
	username string
	team string

	conn *matterclient.MMClient
	handlerStopChan chan bool

	usermap map[string]string		// map username to mm user id
	sentjoinedmap map[string]bool	// map username/room name to bool
	userdisplaynamemap map[UserID]string	 // map username to last displayname
}


func (mm *Mattermost) SetHandler(h Handler) {
	mm.handler = h
}

func (mm *Mattermost) Protocol() string {
	return "mattermost"
}

func (mm *Mattermost) Configure(c Configuration) error {
	if mm.conn != nil {
		mm.Close()
	}

	var err error

	mm.server, err = c.GetString("server")
	if err != nil {
		return err
	}

	mm.username, err = c.GetString("username")
	if err != nil {
		return err
	}

	mm.team, err = c.GetString("team")
	if err != nil {
		return err
	}

	notls, err := c.GetBool("no_tls", false)
	if err != nil {
		return err
	}

	password, _ := c.GetString("password", "")
	token, _ := c.GetString("token", "")
	if token != "" {
		password = "token=" + token
	}
	mm.conn = matterclient.New(mm.username, password, mm.team, mm.server)
	mm.conn.Credentials.NoTLS = notls
	err = mm.conn.Login()
	if err != nil {
		return err
	}
	go mm.conn.WsReceiver()
	go mm.conn.StatusLoop()

	fmt.Printf("CLIENT4: %#v\n", mm.conn.Client)

	for i := 0; i < 42; i++ {
		time.Sleep(time.Duration(1) * time.Second)
		if mm.conn.WsConnected {
			mm.handleConnected()
			return nil
		}
	}
	return fmt.Errorf("Failed to connect after 42s attempting")
}

func (mm *Mattermost) User() UserID {
	return UserID(mm.username + "@" + mm.server)
}

func (mm *Mattermost) getTeamIdByName(name string) string {
	for _, team := range mm.conn.OtherTeams {
		if team.Team.Name == name {
			return team.Id
		}
	}
	return ""
}

func (mm *Mattermost) checkRoomId(id RoomID) (string, error) {
	x := strings.Split(string(id), "@")
	if len(x) == 1 {
		return "", fmt.Errorf("Please write whole room ID with team and server: %s@%s@%s", id, mm.team, mm.server)
	}
	if len(x) == 2 {
		return x[0], nil
	}
	if len(x) != 3 || x[2] != mm.server {
		return "", fmt.Errorf("Invalid room ID: %s", id)
	}

	team_id := mm.getTeamIdByName(x[1])
	if team_id == "" {
		return "", fmt.Errorf("Team not found: %s", id)
	}

	ch_id := mm.conn.GetChannelId(x[0], team_id)
	if ch_id == "" {
		return "", fmt.Errorf("Channel not found: %s", id)
	}
	return ch_id, nil
}

func (mm *Mattermost) reverseRoomId(id string) RoomID {
	team := mm.conn.GetChannelTeamId(id)
	if team == "" {
		return RoomID(fmt.Sprintf("%s@%s", id, mm.server))
	} else {
		teamName := mm.conn.GetTeamName(team)
		name := mm.conn.GetChannelName(id)
		fmt.Printf("CHANNEL NAME: %s TEAM: %s\n", name, teamName)
		return RoomID(fmt.Sprintf("%s@%s@%s", name, teamName, mm.server))
	}
}

func (mm *Mattermost) checkUserId(id UserID) (string, error) {
	x := strings.Split(string(id), "@")
	if len(x) == 1 {
		return "", fmt.Errorf("Please write whole user ID with server: %s@%s", id, mm.server)
	}
	if len(x) != 2 || x[1] != mm.server {
		return "", fmt.Errorf("Invalid user ID: %s", id)
	}
	if user_id, ok := mm.usermap[x[0]]; ok {
		return user_id, nil
	}
	u, resp := mm.conn.Client.GetUserByUsername(x[0], "")
	if u == nil || resp.Error != nil {
		return "", fmt.Errorf("Not found: %s (%s)", x[0], resp.Error)
	}
	mm.usermap[x[0]] = u.Id
	return u.Id, nil
}

func (mm *Mattermost) SetUserInfo(info *UserInfo) error {
	return fmt.Errorf("Not implemented")
}

func (mm *Mattermost) SetRoomInfo(roomId RoomID, info *RoomInfo) error {
	ch, err := mm.checkRoomId(roomId)
	if err != nil {
		return err
	}

	if info.Topic != "" {
		mm.conn.UpdateChannelHeader(ch, info.Topic)
	}

	if info.Picture != nil {
		err = fmt.Errorf("Not supported: channel picture on mattermost")
	}

	if info.Name != "" {
		err = fmt.Errorf("Not supported: channel name on mattermost")
	}

	return err
}

func (mm *Mattermost) Join(roomId RoomID) error {
	ch, err := mm.checkRoomId(roomId)
	if err != nil {
		return err
	}

	return mm.conn.JoinChannel(ch)
}

func (mm *Mattermost) Invite(userId UserID, roomId RoomID) error {
	if roomId == "" {
		_, err := mm.checkUserId(userId)
		return err
	}

	return fmt.Errorf("Not supported: invite on mattermost")
}

func (mm *Mattermost) Leave(roomId RoomID) {
	// Not supported? TODO
}

func (mm *Mattermost) Send(event *Event) error {
	// TODO: attachements
	// TODO: verify private messages work

	post := &model.Post{
		Message: event.Text,
	}
	if event.Type == EVENT_ACTION {
		post.Type = "me"
	}

	if event.Room != "" {
		ch, err := mm.checkRoomId(event.Room)
		if err != nil {
			return err
		}
		post.ChannelId = ch
	} else if event.Recipient != "" {
		ui, err := mm.checkUserId(event.Recipient)
		if err != nil {
			return err
		}

		_, resp := mm.conn.Client.CreateDirectChannel(mm.conn.User.Id, ui)
		if resp.Error != nil {
			return resp.Error
		}
		channelName := model.GetDMNameFromIds(ui, mm.conn.User.Id)

		err = mm.conn.UpdateChannels()
		if err != nil {
			return err
		}

		post.ChannelId = mm.conn.GetChannelId(channelName, "")
	} else {
		return fmt.Errorf("Invalid target")
	}

	_, resp := mm.conn.Client.CreatePost(post)
	if resp.Error != nil {
		return resp.Error
	}
	return nil
}

func (mm *Mattermost) Close() {
	mm.conn.WsQuit = true
	if mm.handlerStopChan != nil {
		mm.handlerStopChan <- true
		mm.handlerStopChan = nil
	}
}

func (mm *Mattermost) handleConnected() {
	mm.handlerStopChan = make(chan bool)
	mm.usermap = make(map[string]string)
	mm.sentjoinedmap = make(map[string]bool)
	mm.userdisplaynamemap = make(map[UserID]string)
	go mm.handleLoop(mm.conn.MessageChan, mm.handlerStopChan)

	fmt.Printf("Connected to mattermost\n")

	chans := mm.conn.GetChannels()
	for _, ch := range chans {
		if len(strings.Split(ch.Name, "__")) == 2 {
			continue // This is a DM channel
		}
		id := mm.reverseRoomId(ch.Id)
		chName := ch.DisplayName
		if teamName := mm.conn.GetTeamName(ch.TeamId); teamName != "" {
			chName = teamName + " / " + chName
		}
		mm.handler.Joined(id)
		mm.handler.RoomInfoUpdated(id, UserID(""), &RoomInfo{
			Name: chName,
			Topic: ch.Header,
		})
	}
}

func (mm *Mattermost) handleLoop(msgCh chan *matterclient.Message, quitCh chan bool) {
	for {
		select {
		case <-quitCh:
			break
		case msg := <-msgCh:
			fmt.Printf("Mattermost: %#v\n", msg)
			fmt.Printf("Mattermost raw: %#v\n", msg.Raw)
			err := mm.handlePosted(msg.Raw)
			if err != nil {
				log.Warnf("Mattermost error: %s", err)
			}
		}
	}
}

func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
	channel_name := msg.Data["channel_name"].(string)
	post_str := msg.Data["post"].(string)
	var post model.Post
	err := json.Unmarshal([]byte(post_str), &post)
	if err != nil {
		return err
	}

	// Skip self messages
	if post.UserId == mm.conn.User.Id {
		return nil
	}

	// Find sending user
	user := mm.conn.GetUser(post.UserId)
	if user == nil {
		return fmt.Errorf("Invalid user")
	}
	userId := UserID(fmt.Sprintf("%s@%s", user.Username, mm.server))

	userDisp := user.GetDisplayName(model.SHOW_NICKNAME_FULLNAME)
	if lastdn, ok := mm.userdisplaynamemap[userId]; !ok || lastdn != userDisp {
		mm.handler.UserInfoUpdated(userId, &UserInfo{
			DisplayName: userDisp,
		})
		mm.userdisplaynamemap[userId] = userDisp
	}

	// Build message event
	msg_ev := &Event{
		Author: userId,
		Text: post.Message,
		Type: EVENT_MESSAGE,
	}
	if post.Type == "me" {
		msg_ev.Type = EVENT_ACTION
	}

	// Handle files
	if post.FileIds != nil && len(post.FileIds) > 0 {
		msg_ev.Attachements = []MediaObject{}
		for _, file := range post.Metadata.Files {
			blob, resp := mm.conn.Client.GetFile(file.Id)
			if resp.Error != nil {
				return resp.Error
			}
			media_object := &BlobMediaObject{
				ObjectFilename: file.Name,
				ObjectSize: file.Size,
				ObjectMimetype: file.MimeType,
				ObjectData: blob,
			}
			if file.Width > 0 {
				media_object.ObjectImageSize = &ImageSize{
					Width: file.Width,
					Height: file.Height,
				}
			}
			msg_ev.Attachements = append(msg_ev.Attachements, media_object)
		}
	}

	// Dispatch as PM or as room message
	if len(strings.Split(channel_name, "__")) == 2 {
		// Private message, no need to find room id
		if user.Id == mm.conn.User.Id {
			// Skip self sent messages
			return nil
		}

		mm.handler.Event(msg_ev)
	} else {
		roomId := mm.reverseRoomId(post.ChannelId)
		if roomId == "" {
			return fmt.Errorf("Invalid channel id")
		}

		cache_key := fmt.Sprintf("%s / %s", userId, roomId)
		if _, ok := mm.sentjoinedmap[cache_key]; !ok {
			mm.handler.Event(&Event{
				Author: userId,
				Room: roomId,
				Type: EVENT_JOIN,
			})
			mm.sentjoinedmap[cache_key] = true
		}

		if post.Type == "system_header_change" {
			new_header := post.Props["new_header"].(string)
			mm.handler.RoomInfoUpdated(roomId, userId, &RoomInfo{
				Topic: new_header,
			})
		} else if post.Type == "" || post.Type == "me" {
			msg_ev.Room = roomId
			mm.handler.Event(msg_ev)
		} else {
			return fmt.Errorf("Unhandled post type: %s", post.Type)
		}
	}
	return nil
}