easybridge/account.go
Alex Auvolat 38a3f1bdb1 Fix Mattermost event deduplication
Mattermost assigns its own IDs to messages, thus when sending a message
to Mattermost the event_seen key that has to be written must take into
account that ID and not the one that we put in the event (which was the
Matrix event ID)

Note that for XMPP anything can be used as an ID, so using the Matrix
event ID there worked, but it's actually not so good.
2020-02-29 10:01:42 +01:00

502 lines
12 KiB
Go

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
}
}
var cache_key string
if event.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)
slot_key := dbKvSlotKey(cache_key)
dbLockSlot(slot_key)
defer dbUnlockSlot(slot_key)
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)
}