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/external" _ "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] // Check we can create connector 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) } // If the account existed already, close and drop connector if prev_acct, ok := accounts[name]; ok { if prev_acct.Protocol == protocol && reflect.DeepEqual(config, prev_acct.Config) { return nil } go prev_acct.Conn.Close() delete(accounts, name) } // Configure and connect 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 CloseAllAccountsForShutdown() { accountsLock.Lock() defer accountsLock.Unlock() for _, accl := range registeredAccounts { for _, acct := range accl { log.Printf("Closing %s %s (%s)", acct.MatrixUser, acct.AccountName, acct.Protocol) acct.Conn.Close() } } } // ---- 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: %s", a.Protocol, a.AccountName, msg) ezbrSystemSend(a.MatrixUser, msg) } func (a *Account) connect() { log.Printf("Connecting %s %s (%s)", a.MatrixUser, a.AccountName, a.Protocol) 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) SystemMessage(msg string) { a.ezbrMessagef("%s", msg) } func (a *Account) SaveConfig(config Configuration) { a.Config = config if key, ok := userKeys[a.MatrixUser]; ok { var entry DbAccountConfig db.Where(&DbAccountConfig{ MxUserID: a.MatrixUser, Name: a.AccountName, }).Assign(&DbAccountConfig{ Protocol: a.Protocol, Config: encryptAccountConfig(a.Config, key), }).FirstOrCreate(&entry) } } 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.MediaObject != nil { cache_key := fmt.Sprintf("%s/user_avatar/%s", a.Protocol, user) cache_val := info.Avatar.Filename() if cache_val == "" || dbKvGet(cache_key) != cache_val { err2 := mx.ProfileAvatar(mx_user_id, info.Avatar) if err2 == nil { dbKvPut(cache_key, cache_val) } else { 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.MediaObject != nil { cache_key := fmt.Sprintf("%s/room_picture/%s", a.Protocol, roomId) cache_val := info.Picture.Filename() if cache_val == "" || dbKvGet(cache_key) != cache_val { err2 := mx.RoomAvatarAs(mx_room_id, info.Picture, as_mxid) if err2 == nil { dbKvPut(cache_key, cache_val) } else { 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 } } var cache_key string if event.Id != "" { // Use mx_room_id as a lock slot key, because this section is // concurrent with the part that sends events out in server.go, // and at that point we don't know the event's ID yet // since it will be attributed by the backend during the call to send dbLockSlot(mx_room_id) defer dbUnlockSlot(mx_room_id) // If the event has an ID, make sure it is processed only once cache_key = fmt.Sprintf("%s/event_seen/%s/%s", a.Protocol, mx_room_id, event.Id) if dbKvGet(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 } } } // Mark message as received in db if cache_key != "" { dbKvPutLocked(cache_key, "yes") } 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) }