aboutsummaryrefslogblamecommitdiff
path: root/account.go
blob: 732ff77e8aad91d956fda86da122213c84b97515 (plain) (tree)
1
2
3
4
5
6
7
8
9
            

        
             
                 
                 
              

                                        
 
                                                             




                                                                        


                     
                          
                          
                          
                                     
 
                             


                                   
                           

                                                         













                                                                                            


                                                                 



                                                 




                                                                           



















                                                                                                 


                                   




                                                        
         
                  


                                                       


                                   








                                                                              


                                   












                                                                             


                                   
                                                    


                                            

                               

 

       

















                                                                                 





                                                                     
                                                                                                                               




                                                                        
                                                                                                       



                 

       





                                                                    
                             

                                                                                                 
                                         




                                                                                                                   













                                                                                                                                      









                                              








                                              
                                 
 
                                         

                                       
                                                                                     



                                                       

                                    

                             

                                                          
                          

         
                                                            
 
                                                     

                                                                               
         
                  

 

       
                                       

                                     
                                                                                   



                                                     

                                     

                             

                                                          
                          

         
                                                          
 
                                                                                                          



                                                                           





                                                                
                       
                                                                                            
         

 
                                                                              

                                                        
                          


                                   
                                                                                                               


                                  
         
 
                               

                                                                               
                                                                            



                                                                         
                 
         

                  

 

       
                                                                                 

                                                              
                                                                                              



                                                                                               

                                                          
                          

         
                             


                                      

                                                                   




                                            
                                                                       


                                  
         

                            
                                                                     
                                                                


                                  


                                

                                                                                  
                                                                            



                                                                                  
                 
         

                  

 

       
                                       

                                     
                                                                                                                             



                                                     


                                                                                                       

                                                                
                          


                                     
                                                                                 

                                                                      
                                  

                 
                                                           
                               
                                                                                 
                                          

                                  

                 
                                                            
                                             
                                                                                 

                                                                      
                                  

                 
                                                             
                
                                                                                

                                
                                        
                                                                             
                                       
                                          
                         

                                                                                                                          
                                       
                                          
                         

                 


                                                                       
                                                              





                                                                                




                                               




                                                                               

                                                                



                                                                   

                                                                      
                                                                      
                                                                                                                    


                                                                        
                                                                                 
                                                                              


                                                                          


                                                                     
                                                                                 
                                                                              
                                                                          








                                                                                                  
         
 





                                                             
                                 




                                                             
                                 
 
package main

import (
	"fmt"
	"reflect"
	"strings"
	"sync"

	log "github.com/sirupsen/logrus"

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

	// Necessary for them to register their protocols
	_ "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/irc"
	_ "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/mattermost"
	_ "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/xmpp"
)

type Account struct {
	MatrixUser  string
	AccountName string
	Protocol    string
	Config      map[string]string

	Conn        Connector
	JoinedRooms map[RoomID]bool
}

var accountsLock sync.Mutex
var registeredAccounts = map[string]map[string]*Account{}

func SetAccount(mxid string, name string, protocol string, config map[string]string) error {
	accountsLock.Lock()
	defer accountsLock.Unlock()

	if _, ok := registeredAccounts[mxid]; !ok {
		registeredAccounts[mxid] = make(map[string]*Account)
	}
	accounts := registeredAccounts[mxid]

	if prev_acct, ok := accounts[name]; ok {
		if protocol != prev_acct.Protocol {
			return fmt.Errorf("Wrong protocol")
		}
		if !reflect.DeepEqual(config, prev_acct.Config) {
			prev_acct.Conn.Close()
			prev_acct.JoinedRooms = map[RoomID]bool{}

			prev_acct.Config = config
			go prev_acct.connect()
		}
	} else {
		proto, ok := Protocols[protocol]
		if !ok {
			return fmt.Errorf("Invalid protocol: %s", protocol)
		}
		conn := proto.NewConnector()
		if conn == nil {
			return fmt.Errorf("Could not create connector for protocol %s", protocol)
		}
		account := &Account{
			MatrixUser:  mxid,
			AccountName: name,
			Protocol:    protocol,
			Config:      config,
			Conn:        conn,
			JoinedRooms: map[RoomID]bool{},
		}
		conn.SetHandler(account)

		accounts[name] = account
		go account.connect()
	}
	return nil
}

func ListAccounts(mxUser string) []*Account {
	accountsLock.Lock()
	defer accountsLock.Unlock()

	ret := []*Account{}
	if accts, ok := registeredAccounts[mxUser]; ok {
		for _, acct := range accts {
			ret = append(ret, acct)
		}
	}
	return ret
}

func FindAccount(mxUser string, name string) *Account {
	accountsLock.Lock()
	defer accountsLock.Unlock()

	if u, ok := registeredAccounts[mxUser]; ok {
		if a, ok := u[name]; ok {
			return a
		}
	}
	return nil
}

func FindJoinedAccount(mxUser string, protocol string, room RoomID) *Account {
	accountsLock.Lock()
	defer accountsLock.Unlock()

	if u, ok := registeredAccounts[mxUser]; ok {
		for _, acct := range u {
			if acct.Protocol == protocol {
				if j, ok := acct.JoinedRooms[room]; ok && j {
					return acct
				}
			}
		}
	}
	return nil
}

func RemoveAccount(mxUser string, name string) {
	accountsLock.Lock()
	defer accountsLock.Unlock()

	if u, ok := registeredAccounts[mxUser]; ok {
		if acct, ok := u[name]; ok {
			acct.Conn.Close()
		}
		delete(u, name)
	}
}

// ----

func SaveDbAccounts(mxid string, key *[32]byte) {
	accountsLock.Lock()
	defer accountsLock.Unlock()

	if accounts, ok := registeredAccounts[mxid]; ok {
		for name, acct := range accounts {
			var entry DbAccountConfig
			db.Where(&DbAccountConfig{
				MxUserID: mxid,
				Name:     name,
			}).Assign(&DbAccountConfig{
				Protocol: acct.Protocol,
				Config:   encryptAccountConfig(acct.Config, key),
			}).FirstOrCreate(&entry)
		}
	}
}

func LoadDbAccounts(mxid string, key *[32]byte) {
	var allAccounts []DbAccountConfig
	db.Where(&DbAccountConfig{MxUserID: mxid}).Find(&allAccounts)
	for _, acct := range allAccounts {
		config, err := decryptAccountConfig(acct.Config, key)
		if err != nil {
			ezbrSystemSendf("Could not decrypt stored configuration for account %s (%s)", acct.Name, acct.Protocol)
			continue
		}

		err = SetAccount(mxid, acct.Name, acct.Protocol, config)
		if err != nil {
			ezbrSystemSendf(mxid, "Could not setup account %s: %s", acct.Name, err.Error())
		}
	}
}

// ----

func (a *Account) ezbrMessagef(format string, args ...interface{}) {
	msg := fmt.Sprintf(format, args...)
	msg = fmt.Sprintf("%s: %s", a.Protocol, msg)
	ezbrSystemSend(a.MatrixUser, msg)
}

func (a *Account) connect() {
	ezbrSystemSendf(a.MatrixUser, "Connecting to account %s (%s)", a.AccountName, a.Protocol)

	err := a.Conn.Configure(a.Config)
	if err != nil {
		ezbrSystemSendf(a.MatrixUser, "%s (%s) cannot connect: %s", a.AccountName, a.Protocol, err.Error())
		return
	}

	var autojoin []DbJoinedRoom
	db.Where(&DbJoinedRoom{
		MxUserID:    a.MatrixUser,
		Protocol:    a.Protocol,
		AccountName: a.AccountName,
	}).Find(&autojoin)
	for _, aj := range autojoin {
		err := a.Conn.Join(aj.RoomID)
		if err != nil {
			ezbrSystemSendf(a.MatrixUser, "%s (%s) cannot join %s: %s", a.AccountName, a.Protocol, aj.RoomID, err.Error())
		}
	}
}

func (a *Account) addAutojoin(roomId RoomID) {
	var entry DbJoinedRoom
	db.Where(&DbJoinedRoom{
		MxUserID:    a.MatrixUser,
		Protocol:    a.Protocol,
		AccountName: a.AccountName,
		RoomID:      roomId,
	}).FirstOrCreate(&entry)
}

func (a *Account) delAutojoin(roomId RoomID) {
	db.Where(&DbJoinedRoom{
		MxUserID:    a.MatrixUser,
		Protocol:    a.Protocol,
		AccountName: a.AccountName,
		RoomID:      roomId,
	}).Delete(&DbJoinedRoom{})
}

// ---- Begin event handlers ----

func (a *Account) Joined(roomId RoomID) {
	err := a.joinedInternal(roomId)
	if err != nil {
		a.ezbrMessagef("Dropping Account.Joined %s: %s", roomId, err.Error())
	}
}

func (a *Account) joinedInternal(roomId RoomID) error {
	a.JoinedRooms[roomId] = true

	a.addAutojoin(roomId)

	mx_room_id, err := dbGetMxRoom(a.Protocol, roomId)
	if err != nil {
		return err
	}

	log.Tracef("Joined %s (%s)\n", roomId, a.MatrixUser)

	err = mx.RoomInvite(mx_room_id, a.MatrixUser)
	if err != nil && strings.Contains(err.Error(), "already in the room") {
		err = nil
	}
	return err
}

// ----

func (a *Account) Left(roomId RoomID) {
	err := a.leftInternal(roomId)
	if err != nil {
		a.ezbrMessagef("Dropping Account.Left %s: %s", roomId, err.Error())
	}
}

func (a *Account) leftInternal(roomId RoomID) error {
	delete(a.JoinedRooms, roomId)

	a.delAutojoin(roomId)

	mx_room_id, err := dbGetMxRoom(a.Protocol, roomId)
	if err != nil {
		return err
	}

	log.Tracef("Left %s (%s)\n", roomId, a.MatrixUser)

	err = mx.RoomKick(mx_room_id, a.MatrixUser, fmt.Sprintf("got leave room event on %s", a.Protocol))
	if err != nil && strings.Contains(err.Error(), "not in the room") {
		err = nil
	}
	return err
}

// ----

func (a *Account) UserInfoUpdated(user UserID, info *UserInfo) {
	err := a.userInfoUpdatedInternal(user, info)
	if err != nil {
		a.ezbrMessagef("Dropping Account.UserInfoUpdated %s: %s", user, err.Error())
	}
}

func (a *Account) userInfoUpdatedInternal(user UserID, info *UserInfo) error {
	mx_user_id, err := dbGetMxUser(a.Protocol, user)
	if err != nil {
		return err
	}

	if info.DisplayName != "" {
		err2 := mx.ProfileDisplayname(mx_user_id, fmt.Sprintf("%s (%s)", info.DisplayName, a.Protocol))
		if err2 != nil {
			err = err2
		}
	}

	if info.Avatar != nil {
		cache_key := fmt.Sprintf("%s/user_avatar/%s", a.Protocol, user)
		cache_val := info.Avatar.Filename()
		if cache_val == "" || dbKvTestAndSet(cache_key, cache_val) {
			err2 := mx.ProfileAvatar(mx_user_id, info.Avatar)
			if err2 != nil {
				err = err2
			}
		}
	}

	return err
}

// ----

func (a *Account) RoomInfoUpdated(roomId RoomID, author UserID, info *RoomInfo) {
	err := a.roomInfoUpdatedInternal(roomId, author, info)
	if err != nil {
		a.ezbrMessagef("Dropping Account.RoomInfoUpdated %s: %s", roomId, err.Error())
	}
}

func (a *Account) roomInfoUpdatedInternal(roomId RoomID, author UserID, info *RoomInfo) error {
	mx_room_id, err := dbGetMxRoom(a.Protocol, roomId)
	if err != nil {
		return err
	}

	as_mxid := ezbrMxId()
	if author == a.Conn.User() {
		as_mxid = a.MatrixUser
	} else if len(author) > 0 {
		mx_user_id, err2 := dbGetMxUser(a.Protocol, author)
		if err2 == nil {
			as_mxid = mx_user_id
		}
	}

	if info.Topic != "" {
		err2 := mx.RoomTopicAs(mx_room_id, info.Topic, as_mxid)
		if err2 != nil {
			err = err2
		}
	}

	if info.Name != "" {
		name := fmt.Sprintf("%s (%s)", info.Name, a.Protocol)
		err2 := mx.RoomNameAs(mx_room_id, name, as_mxid)
		if err2 != nil {
			err = err2
		}
	}

	if info.Picture != nil {
		cache_key := fmt.Sprintf("%s/room_picture/%s", a.Protocol, roomId)
		cache_val := info.Picture.Filename()
		if cache_val == "" || dbKvTestAndSet(cache_key, cache_val) {
			err2 := mx.RoomAvatarAs(mx_room_id, info.Picture, as_mxid)
			if err2 != nil {
				err = err2
			}
		}
	}

	return err
}

// ----

func (a *Account) Event(event *Event) {
	err := a.eventInternal(event)
	if err != nil {
		a.ezbrMessagef("Dropping Account.Event %s %s %s: %s", event.Author, event.Recipient, event.Room, err.Error())
	}
}

func (a *Account) eventInternal(event *Event) error {
	// TODO: automatically ignore events that come from one of our bridged matrix users
	// TODO: deduplicate events if we have several matrix users joined the same room (hard problem)

	mx_user_id, err := dbGetMxUser(a.Protocol, event.Author)
	if err != nil {
		return err
	}

	if event.Type == EVENT_JOIN {
		log.Tracef("%s join %s %s", a.Protocol, event.Author, event.Room)
		mx_room_id, err := dbGetMxRoom(a.Protocol, event.Room)
		if err != nil {
			return err
		}

		err = mx.RoomInvite(mx_room_id, mx_user_id)
		if err != nil {
			if strings.Contains(err.Error(), "already in the room") {
				return nil
			}
			return err
		}

		return mx.RoomJoinAs(mx_room_id, mx_user_id)
	} else if event.Type == EVENT_LEAVE {
		log.Tracef("%s join %s %s", a.Protocol, event.Author, event.Room)
		mx_room_id, err := dbGetMxRoom(a.Protocol, event.Room)
		if err != nil {
			return err
		}

		return mx.RoomLeaveAs(mx_room_id, mx_user_id)
	} else {
		log.Tracef("%s msg %s %s", a.Protocol, event.Author, event.Room)
		mx_room_id := ""

		if len(event.Room) > 0 {
			mx_room_id, err = dbGetMxRoom(a.Protocol, event.Room)
			if err != nil {
				return err
			}
		} else {
			mx_room_id, err = dbGetMxPmRoom(a.Protocol, event.Author, mx_user_id, a.MatrixUser, a.AccountName)
			if err != nil {
				return err
			}
		}

		if event.Id != "" {
			cache_key := fmt.Sprintf("%s/event_seen/%s/%s",
				a.Protocol, mx_room_id, event.Id)
			if !dbKvTestAndSet(cache_key, "yes") {
				// false: cache key was not modified, meaning we
				// already saw the event
				return nil
			}
		}

		typ := "m.text"
		if event.Type == EVENT_ACTION {
			typ = "m.emote"
		}

		err = mx.SendMessageAs(mx_room_id, typ, event.Text, mx_user_id)
		if err != nil {
			return err
		}

		if event.Attachments != nil {
			for _, file := range event.Attachments {
				mxfile, err := mx.UploadMedia(file)
				if err != nil {
					return err
				}
				content := map[string]interface{}{
					"body":     mxfile.Filename(),
					"filename": mxfile.Filename(),
					"url":      fmt.Sprintf("mxc://%s/%s", mxfile.MxcServer, mxfile.MxcMediaId),
				}
				if sz := mxfile.ImageSize(); sz != nil {
					content["msgtype"] = "m.image"
					content["info"] = map[string]interface{}{
						"mimetype": mxfile.Mimetype(),
						"size":     mxfile.Size(),
						"w":        sz.Width,
						"h":        sz.Height,
					}
				} else {
					content["msgtype"] = "m.file"
					content["info"] = map[string]interface{}{
						"mimetype": mxfile.Mimetype(),
						"size":     mxfile.Size(),
					}
				}
				err = mx.SendAs(mx_room_id, "m.room.message", content, mx_user_id)
				if err != nil {
					return err
				}
			}
		}
		return nil
	}
}

// ----

func (a *Account) CacheGet(key string) string {
	cache_key := fmt.Sprintf("%s/account/%s/%s/%s",
		a.Protocol, a.MatrixUser, a.AccountName, key)
	return dbKvGet(cache_key)
}

func (a *Account) CachePut(key string, value string) {
	cache_key := fmt.Sprintf("%s/account/%s/%s/%s",
		a.Protocol, a.MatrixUser, a.AccountName, key)
	dbKvPut(cache_key, value)
}