Alex Auvolat
38a3f1bdb1
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.
502 lines
12 KiB
Go
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)
|
|
}
|