From 67c7f7361d63a282788f159494a6f43172c8806a Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 26 Feb 2020 16:07:33 +0100 Subject: Move appservice/ to / --- account.go | 331 ++++++++++++++++++++++++++++++++++++++++++++++++++ appservice/account.go | 331 -------------------------------------------------- appservice/db.go | 266 ---------------------------------------- appservice/server.go | 265 ---------------------------------------- appservice/util.go | 58 --------- db.go | 266 ++++++++++++++++++++++++++++++++++++++++ main.go | 17 +-- server.go | 259 +++++++++++++++++++++++++++++++++++++++ util.go | 58 +++++++++ 9 files changed, 918 insertions(+), 933 deletions(-) create mode 100644 account.go delete mode 100644 appservice/account.go delete mode 100644 appservice/db.go delete mode 100644 appservice/server.go delete mode 100644 appservice/util.go create mode 100644 db.go create mode 100644 server.go create mode 100644 util.go diff --git a/account.go b/account.go new file mode 100644 index 0000000..8da6d44 --- /dev/null +++ b/account.go @@ -0,0 +1,331 @@ +package main + +import ( + "fmt" + "strings" + + log "github.com/sirupsen/logrus" + + . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" +) + +type Account struct { + MatrixUser string + AccountName string + Protocol string + Conn Connector + + JoinedRooms map[RoomID]bool +} + +var registeredAccounts = map[string]map[string]*Account{} + +func AddAccount(a *Account) { + if _, ok := registeredAccounts[a.MatrixUser]; !ok { + registeredAccounts[a.MatrixUser] = make(map[string]*Account) + } + registeredAccounts[a.MatrixUser][a.AccountName] = a + ezbrSystemSendf(a.MatrixUser, "Connecting to account %s (%s)", a.AccountName, a.Protocol) +} + +func FindAccount(mxUser string, name string) *Account { + 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 { + 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) { + if u, ok := registeredAccounts[mxUser]; ok { + delete(u, name) + } +} + +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) +} + +// ---- 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 + + mx_room_id, err := dbGetMxRoom(a.Protocol, roomId) + if err != nil { + return err + } + + log.Debugf("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) + + mx_room_id, err := dbGetMxRoom(a.Protocol, roomId) + if err != nil { + return err + } + + log.Printf("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 == "" || dbCacheTestAndSet(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 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 == "" || dbCacheTestAndSet(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.Printf("%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.Printf("%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.Printf("%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 !dbCacheTestAndSet(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 dbCacheGet(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) + dbCachePut(cache_key, value) +} diff --git a/appservice/account.go b/appservice/account.go deleted file mode 100644 index 4bb2dfe..0000000 --- a/appservice/account.go +++ /dev/null @@ -1,331 +0,0 @@ -package appservice - -import ( - "fmt" - "strings" - - log "github.com/sirupsen/logrus" - - . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" -) - -type Account struct { - MatrixUser string - AccountName string - Protocol string - Conn Connector - - JoinedRooms map[RoomID]bool -} - -var registeredAccounts = map[string]map[string]*Account{} - -func AddAccount(a *Account) { - if _, ok := registeredAccounts[a.MatrixUser]; !ok { - registeredAccounts[a.MatrixUser] = make(map[string]*Account) - } - registeredAccounts[a.MatrixUser][a.AccountName] = a - ezbrSystemSendf(a.MatrixUser, "Connecting to account %s (%s)", a.AccountName, a.Protocol) -} - -func FindAccount(mxUser string, name string) *Account { - 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 { - 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) { - if u, ok := registeredAccounts[mxUser]; ok { - delete(u, name) - } -} - -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) -} - -// ---- 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 - - mx_room_id, err := dbGetMxRoom(a.Protocol, roomId) - if err != nil { - return err - } - - log.Debugf("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) - - mx_room_id, err := dbGetMxRoom(a.Protocol, roomId) - if err != nil { - return err - } - - log.Printf("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 == "" || dbCacheTestAndSet(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 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 == "" || dbCacheTestAndSet(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.Printf("%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.Printf("%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.Printf("%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 !dbCacheTestAndSet(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 dbCacheGet(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) - dbCachePut(cache_key, value) -} diff --git a/appservice/db.go b/appservice/db.go deleted file mode 100644 index 34fc046..0000000 --- a/appservice/db.go +++ /dev/null @@ -1,266 +0,0 @@ -package appservice - -import ( - "fmt" - "sync" - - "github.com/jinzhu/gorm" - _ "github.com/jinzhu/gorm/dialects/mysql" - _ "github.com/jinzhu/gorm/dialects/postgres" - _ "github.com/jinzhu/gorm/dialects/sqlite" - log "github.com/sirupsen/logrus" - "golang.org/x/crypto/blake2b" - - "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" - "git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib" -) - -var db *gorm.DB - -func InitDb() error { - var err error - - db, err = gorm.Open(config.DbType, config.DbPath) - if err != nil { - return err - } - - db.AutoMigrate(&DbCache{}) - - db.AutoMigrate(&DbUserMap{}) - db.Model(&DbUserMap{}).AddIndex("idx_protocol_user", "protocol", "user_id") - - db.AutoMigrate(&DbRoomMap{}) - db.Model(&DbRoomMap{}).AddIndex("idx_protocol_room", "protocol", "room_id") - - db.AutoMigrate(&DbPmRoomMap{}) - db.Model(&DbPmRoomMap{}).AddIndex("idx_protocol_user_account_user", "protocol", "user_id", "mx_user_id", "account_name") - - return nil -} - -// Long-term cache entries -type DbCache struct { - gorm.Model - - Key string `gorm:"unique_index"` - Value string -} - -// User mapping between protocol user IDs and puppeted matrix ids -type DbUserMap struct { - gorm.Model - - Protocol string - UserID connector.UserID - MxUserID string `gorm:"index:mxuserid"` -} - -// Room mapping between Matrix rooms and outside rooms -type DbRoomMap struct { - gorm.Model - - // Network protocol - Protocol string - - // Room id on the bridged network - RoomID connector.RoomID - - // Bridged room matrix id - MxRoomID string `gorm:"index:mxroomid"` -} - -// Room mapping between Matrix rooms and private messages -type DbPmRoomMap struct { - gorm.Model - - // User id and account name of the local end viewed on Matrix - MxUserID string - Protocol string - AccountName string - - // User id to reach them - UserID connector.UserID - - // Bridged room for PMs - MxRoomID string `gorm:"index:mxroomoid"` -} - -// ---- Simple locking mechanism - -var dbLocks [256]sync.Mutex - -func dbLockSlot(key string) { - slot := blake2b.Sum512([]byte(key))[0] - dbLocks[slot].Lock() -} - -func dbUnlockSlot(key string) { - slot := blake2b.Sum512([]byte(key))[0] - dbLocks[slot].Unlock() -} - -// ---- - -func dbCacheGet(key string) string { - var entry DbCache - if db.Where(&DbCache{Key: key}).First(&entry).RecordNotFound() { - return "" - } else { - return entry.Value - } -} - -func dbCachePut(key string, value string) { - var entry DbCache - db.Where(&DbCache{Key: key}).Assign(&DbCache{Value: value}).FirstOrCreate(&entry) -} - -func dbCacheTestAndSet(key string, value string) bool { - dbLockSlot(key) - defer dbUnlockSlot(key) - - // True if value was changed, false if was already set - if dbCacheGet(key) != value { - dbCachePut(key, value) - return true - } - return false -} - -func dbGetMxRoom(protocol string, roomId connector.RoomID) (string, error) { - slot_key := fmt.Sprintf("room: %s / %s", protocol, roomId) - dbLockSlot(slot_key) - defer dbUnlockSlot(slot_key) - - var room DbRoomMap - - // Check if room exists in our mapping, - // If not create it - must_create := db.First(&room, DbRoomMap{ - Protocol: protocol, - RoomID: roomId, - }).RecordNotFound() - if must_create { - alias := roomAlias(protocol, roomId) - // Lookup alias - mx_room_id, err := mx.DirectoryRoom(fmt.Sprintf("#%s:%s", alias, config.MatrixDomain)) - - // If no alias found, create room - if err != nil { - name := fmt.Sprintf("%s (%s)", roomId, protocol) - - mx_room_id, err = mx.CreateRoom(name, alias, []string{}) - if err != nil { - log.Printf("Could not create room for %s: %s", name, err) - return "", err - } - } - - room = DbRoomMap{ - Protocol: protocol, - RoomID: roomId, - MxRoomID: mx_room_id, - } - db.Create(&room) - } - log.Debugf("Got room id: %s", room.MxRoomID) - - return room.MxRoomID, nil -} - -func dbGetMxPmRoom(protocol string, them connector.UserID, themMxId string, usMxId string, usAccount string) (string, error) { - slot_key := fmt.Sprintf("pmroom: %s / %s / %s / %s", protocol, usMxId, usAccount, them) - dbLockSlot(slot_key) - defer dbUnlockSlot(slot_key) - - var room DbPmRoomMap - - must_create := db.First(&room, DbPmRoomMap{ - MxUserID: usMxId, - Protocol: protocol, - AccountName: usAccount, - UserID: them, - }).RecordNotFound() - if must_create { - name := fmt.Sprintf("%s (%s)", them, protocol) - - mx_room_id, err := mx.CreateDirectRoomAs([]string{usMxId}, themMxId) - if err != nil { - log.Printf("Could not create room for %s: %s", name, err) - return "", err - } - - //err = mxRoomJoinAs(mx_room_id, themMxId) - //if err != nil { - // log.Printf("Could not join %s as %s", mx_room_id, themMxId) - // return "", err - //} - - room = DbPmRoomMap{ - MxUserID: usMxId, - Protocol: protocol, - AccountName: usAccount, - UserID: them, - MxRoomID: mx_room_id, - } - db.Create(&room) - } - log.Debugf("Got PM room id: %s", room.MxRoomID) - - return room.MxRoomID, nil -} - -func dbGetMxUser(protocol string, userId connector.UserID) (string, error) { - slot_key := fmt.Sprintf("user: %s / %s", protocol, userId) - dbLockSlot(slot_key) - defer dbUnlockSlot(slot_key) - - var user DbUserMap - - must_create := db.First(&user, DbUserMap{ - Protocol: protocol, - UserID: userId, - }).RecordNotFound() - if must_create { - username := userMxId(protocol, userId) - - err := mx.RegisterUser(username) - if err != nil { - if mxE, ok := err.(*mxlib.MxError); !ok || mxE.ErrCode != "M_USER_IN_USE" { - log.Printf("Could not register %s: %s", username, err) - return "", err - } - } - - mxid := fmt.Sprintf("@%s:%s", username, config.MatrixDomain) - mx.ProfileDisplayname(mxid, fmt.Sprintf("%s (%s)", userId, protocol)) - - user = DbUserMap{ - Protocol: protocol, - UserID: userId, - MxUserID: mxid, - } - db.Create(&user) - } - - return user.MxUserID, nil -} - -func dbIsPmRoom(mxRoomId string) *DbPmRoomMap { - var pm_room DbPmRoomMap - if db.First(&pm_room, DbPmRoomMap{MxRoomID: mxRoomId}).RecordNotFound() { - return nil - } else { - return &pm_room - } -} - -func dbIsPublicRoom(mxRoomId string) *DbRoomMap { - var room DbRoomMap - if db.First(&room, DbRoomMap{MxRoomID: mxRoomId}).RecordNotFound() { - return nil - } else { - return &room - } -} diff --git a/appservice/server.go b/appservice/server.go deleted file mode 100644 index 669559d..0000000 --- a/appservice/server.go +++ /dev/null @@ -1,265 +0,0 @@ -package appservice - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/gorilla/mux" - log "github.com/sirupsen/logrus" - - "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" - "git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib" -) - -type Config struct { - HttpBindAddr string - Server string - DbType string - DbPath string - MatrixDomain string -} - -var registration *mxlib.Registration -var config *Config - -var mx *mxlib.Client - -func Start(r *mxlib.Registration, c *Config) (chan error, error) { - registration = r - config = c - - mx = mxlib.NewClient(c.Server, r.AsToken) - - err := InitDb() - if err != nil { - return nil, err - } - - err = mx.RegisterUser(registration.SenderLocalpart) - if mxe, ok := err.(*mxlib.MxError); !ok || mxe.ErrCode != "M_USER_IN_USE" { - return nil, err - } - if err == nil { - // If Easybridge account was created, update avatar and display name - err = mx.ProfileAvatar(ezbrMxId(), &connector.FileMediaObject{ - Path: "easybridge.jpg", - }) - if err != nil { - return nil, err - } - err = mx.ProfileDisplayname(ezbrMxId(), fmt.Sprintf("Easybridge (%s)", EASYBRIDGE_SYSTEM_PROTOCOL)) - if err != nil { - return nil, err - } - } - - router := mux.NewRouter() - router.HandleFunc("/_matrix/app/v1/transactions/{txnId}", handleTxn) - router.HandleFunc("/transactions/{txnId}", handleTxn) - - errch := make(chan error) - go func() { - log.Printf("Starting HTTP server on %s", config.HttpBindAddr) - err := http.ListenAndServe(config.HttpBindAddr, checkTokenAndLog(router)) - if err != nil { - errch <- err - } - }() - - return errch, nil -} - -func checkTokenAndLog(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - if strings.Join(r.Form["access_token"], "") != registration.HsToken { - http.Error(w, "Wrong or no token provided", http.StatusUnauthorized) - return - } - - log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) - handler.ServeHTTP(w, r) - }) -} - -func handleTxn(w http.ResponseWriter, r *http.Request) { - if r.Method == "PUT" { - var txn mxlib.Transaction - err := json.NewDecoder(r.Body).Decode(&txn) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - log.Warnf("JSON decode error: %s\n", err) - return - } - - log.Debugf("Got transaction %#v\n", txn) - - for i := range txn.Events { - ev := &txn.Events[i] - if strings.HasPrefix(ev.Sender, "@"+registration.SenderLocalpart) { - // Don't do anything with ezbr events that come back to us - continue - } - err = handleTxnEvent(ev) - if err != nil { - ezbrSystemSend(ev.Sender, fmt.Sprintf("Could not process %s (%s): %s", ev.Type, ev.Sender, err)) - } - } - - fmt.Fprintf(w, "{}\n") - } else { - http.Error(w, "Expected PUT request", http.StatusBadRequest) - } -} - -func handleTxnEvent(e *mxlib.Event) error { - if e.Type == "m.room.message" { - ev := &connector.Event{ - Type: connector.EVENT_MESSAGE, - Text: e.Content["body"].(string), - } - typ := e.Content["msgtype"].(string) - if typ == "m.emote" { - ev.Type = connector.EVENT_MESSAGE - } else if typ == "m.file" || typ == "m.image" { - ev.Text = "" - ev.Attachments = []connector.MediaObject{mx.ParseMediaInfo(e.Content)} - } - - if pm_room := dbIsPmRoom(e.RoomId); pm_room != nil { - if pm_room.Protocol == EASYBRIDGE_SYSTEM_PROTOCOL { - handleSystemMessage(e.Sender, e.Content["body"].(string)) - return nil - } - // If this is a private message room - acct := FindAccount(pm_room.MxUserID, pm_room.AccountName) - if acct == nil { - return fmt.Errorf("Not connected to %s", pm_room.AccountName) - } else if e.Sender == pm_room.MxUserID { - ev.Author = acct.Conn.User() - ev.Recipient = pm_room.UserID - return acct.Conn.Send(ev) - } - } else if room := dbIsPublicRoom(e.RoomId); room != nil { - // If this is a regular room - acct := FindJoinedAccount(e.Sender, room.Protocol, room.RoomID) - if acct != nil { - ev.Author = acct.Conn.User() - ev.Room = room.RoomID - return acct.Conn.Send(ev) - } else { - mx.RoomKick(e.RoomId, e.Sender, fmt.Sprintf("Not present in %s on %s, please talk with Easybridge to rejoin", room.RoomID, room.Protocol)) - return fmt.Errorf("not joined %s on %s", room.RoomID, room.Protocol) - } - } else { - return fmt.Errorf("Room not bridged") - } - } else if e.Type == "m.room.member" { - ms := e.Content["membership"].(string) - if ms == "leave" { - if pm_room := dbIsPmRoom(e.RoomId); pm_room != nil { - // If leaving a PM room, we must delete it - them_mx := userMxId(pm_room.Protocol, pm_room.UserID) - mx.RoomLeaveAs(e.RoomId, them_mx) - db.Delete(pm_room) - return nil - } else if room := dbIsPublicRoom(e.RoomId); room != nil { - // If leaving a public room, leave from server as well - acct := FindJoinedAccount(e.Sender, room.Protocol, room.RoomID) - if acct != nil { - acct.Conn.Leave(room.RoomID) - return nil - // TODO: manage autojoin list, remove this room - } else { - mx.RoomKick(e.RoomId, e.Sender, fmt.Sprintf("Not present in %s on %s, please talk with Easybridge to rejoin", room.RoomID, room.Protocol)) - return fmt.Errorf("not joined %s on %s", room.RoomID, room.Protocol) - } - } else { - return fmt.Errorf("Room not bridged") - } - } - } else if e.Type == "m.room.topic" { - if room := dbIsPublicRoom(e.RoomId); room != nil { - acct := FindJoinedAccount(e.Sender, room.Protocol, room.RoomID) - if acct != nil { - return acct.Conn.SetRoomInfo(room.RoomID, &connector.RoomInfo{ - Topic: e.Content["topic"].(string), - }) - } else { - return fmt.Errorf("Could not find room account for %s %s %s", e.Sender, room.Protocol, room.RoomID) - } - } - } - return nil -} - -func handleSystemMessage(mxid string, msg string) { - cmd := strings.Fields(msg) - switch cmd[0] { - case "help": - ezbrSystemSend(mxid, "Welcome to Easybridge! Here is a list of available commands:") - ezbrSystemSend(mxid, "- help: request help") - ezbrSystemSend(mxid, "- list: list accounts") - ezbrSystemSend(mxid, "- accounts: list accounts") - ezbrSystemSend(mxid, "- join : join public chat room") - ezbrSystemSend(mxid, "- talk : open private conversation to contact") - case "list", "account", "accounts": - one := false - if accts, ok := registeredAccounts[mxid]; ok { - for name, acct := range accts { - one = true - ezbrSystemSendf(mxid, "- %s (%s)", name, acct.Protocol) - } - } - if !one { - ezbrSystemSendf(mxid, "No account currently configured") - } - case "join": - account := findAccount(mxid, cmd[1]) - if account != nil { - err := account.Conn.Join(connector.RoomID(cmd[2])) - if err != nil { - ezbrSystemSendf(mxid, "%s", err) - } - } else { - ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1]) - } - case "talk": - account := findAccount(mxid, cmd[1]) - if account != nil { - quser := connector.UserID(cmd[2]) - err := account.Conn.Invite(quser, connector.RoomID("")) - if err != nil { - ezbrSystemSendf(mxid, "%s", err) - return - } - - quser_mxid, err := dbGetMxUser(account.Protocol, quser) - if err != nil { - ezbrSystemSendf(mxid, "%s", err) - return - } - _, err = dbGetMxPmRoom(account.Protocol, quser, quser_mxid, mxid, account.AccountName) - if err != nil { - ezbrSystemSendf(mxid, "%s", err) - } - } else { - ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1]) - } - default: - ezbrSystemSend(mxid, "Unrecognized command. Type `help` if you need some help!") - } -} - -func findAccount(mxid string, q string) *Account { - if accts, ok := registeredAccounts[mxid]; ok { - for name, acct := range accts { - if name == q || acct.Protocol == q { - return acct - } - } - } - return nil -} diff --git a/appservice/util.go b/appservice/util.go deleted file mode 100644 index 160f492..0000000 --- a/appservice/util.go +++ /dev/null @@ -1,58 +0,0 @@ -package appservice - -import ( - "fmt" - "unicode" - - log "github.com/sirupsen/logrus" - - . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" -) - -const EASYBRIDGE_SYSTEM_PROTOCOL string = "✯◡✯" - -func ezbrMxId() string { - return fmt.Sprintf("@%s:%s", registration.SenderLocalpart, config.MatrixDomain) -} - -func ezbrSystemRoom(user_mx_id string) (string, error) { - return dbGetMxPmRoom(EASYBRIDGE_SYSTEM_PROTOCOL, UserID("Easybridge"), ezbrMxId(), user_mx_id, "easybridge") -} - -func ezbrSystemSend(user_mx_id string, msg string) { - mx_room_id, err := ezbrSystemRoom(user_mx_id) - if err == nil { - err = mx.SendMessageAs(mx_room_id, "m.text", msg, ezbrMxId()) - } - if err != nil { - log.Warnf("(%s) %s", user_mx_id, msg) - } -} - -func ezbrSystemSendf(user_mx_id string, format string, args ...interface{}) { - ezbrSystemSend(user_mx_id, fmt.Sprintf(format, args...)) -} - -// ---- - -func roomAlias(protocol string, id RoomID) string { - return fmt.Sprintf("_ezbr__%s__%s", safeStringForId(string(id)), protocol) -} - -func userMxId(protocol string, id UserID) string { - return fmt.Sprintf("_ezbr__%s__%s", safeStringForId(string(id)), protocol) -} - -func safeStringForId(in string) string { - id2 := "" - for _, c := range in { - if c == '@' { - id2 += "__" - } else if c == ':' { - id2 += "_" - } else if unicode.IsDigit(c) || unicode.IsLetter(c) { - id2 += string(c) - } - } - return id2 -} diff --git a/db.go b/db.go new file mode 100644 index 0000000..fe3d1e3 --- /dev/null +++ b/db.go @@ -0,0 +1,266 @@ +package main + +import ( + "fmt" + "sync" + + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/mysql" + _ "github.com/jinzhu/gorm/dialects/postgres" + _ "github.com/jinzhu/gorm/dialects/sqlite" + log "github.com/sirupsen/logrus" + "golang.org/x/crypto/blake2b" + + "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" + "git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib" +) + +var db *gorm.DB + +func InitDb() error { + var err error + + db, err = gorm.Open(config.DbType, config.DbPath) + if err != nil { + return err + } + + db.AutoMigrate(&DbCache{}) + + db.AutoMigrate(&DbUserMap{}) + db.Model(&DbUserMap{}).AddIndex("idx_protocol_user", "protocol", "user_id") + + db.AutoMigrate(&DbRoomMap{}) + db.Model(&DbRoomMap{}).AddIndex("idx_protocol_room", "protocol", "room_id") + + db.AutoMigrate(&DbPmRoomMap{}) + db.Model(&DbPmRoomMap{}).AddIndex("idx_protocol_user_account_user", "protocol", "user_id", "mx_user_id", "account_name") + + return nil +} + +// Long-term cache entries +type DbCache struct { + gorm.Model + + Key string `gorm:"unique_index"` + Value string +} + +// User mapping between protocol user IDs and puppeted matrix ids +type DbUserMap struct { + gorm.Model + + Protocol string + UserID connector.UserID + MxUserID string `gorm:"index:mxuserid"` +} + +// Room mapping between Matrix rooms and outside rooms +type DbRoomMap struct { + gorm.Model + + // Network protocol + Protocol string + + // Room id on the bridged network + RoomID connector.RoomID + + // Bridged room matrix id + MxRoomID string `gorm:"index:mxroomid"` +} + +// Room mapping between Matrix rooms and private messages +type DbPmRoomMap struct { + gorm.Model + + // User id and account name of the local end viewed on Matrix + MxUserID string + Protocol string + AccountName string + + // User id to reach them + UserID connector.UserID + + // Bridged room for PMs + MxRoomID string `gorm:"index:mxroomoid"` +} + +// ---- Simple locking mechanism + +var dbLocks [256]sync.Mutex + +func dbLockSlot(key string) { + slot := blake2b.Sum512([]byte(key))[0] + dbLocks[slot].Lock() +} + +func dbUnlockSlot(key string) { + slot := blake2b.Sum512([]byte(key))[0] + dbLocks[slot].Unlock() +} + +// ---- + +func dbCacheGet(key string) string { + var entry DbCache + if db.Where(&DbCache{Key: key}).First(&entry).RecordNotFound() { + return "" + } else { + return entry.Value + } +} + +func dbCachePut(key string, value string) { + var entry DbCache + db.Where(&DbCache{Key: key}).Assign(&DbCache{Value: value}).FirstOrCreate(&entry) +} + +func dbCacheTestAndSet(key string, value string) bool { + dbLockSlot(key) + defer dbUnlockSlot(key) + + // True if value was changed, false if was already set + if dbCacheGet(key) != value { + dbCachePut(key, value) + return true + } + return false +} + +func dbGetMxRoom(protocol string, roomId connector.RoomID) (string, error) { + slot_key := fmt.Sprintf("room: %s / %s", protocol, roomId) + dbLockSlot(slot_key) + defer dbUnlockSlot(slot_key) + + var room DbRoomMap + + // Check if room exists in our mapping, + // If not create it + must_create := db.First(&room, DbRoomMap{ + Protocol: protocol, + RoomID: roomId, + }).RecordNotFound() + if must_create { + alias := roomAlias(protocol, roomId) + // Lookup alias + mx_room_id, err := mx.DirectoryRoom(fmt.Sprintf("#%s:%s", alias, config.MatrixDomain)) + + // If no alias found, create room + if err != nil { + name := fmt.Sprintf("%s (%s)", roomId, protocol) + + mx_room_id, err = mx.CreateRoom(name, alias, []string{}) + if err != nil { + log.Printf("Could not create room for %s: %s", name, err) + return "", err + } + } + + room = DbRoomMap{ + Protocol: protocol, + RoomID: roomId, + MxRoomID: mx_room_id, + } + db.Create(&room) + } + log.Debugf("Got room id: %s", room.MxRoomID) + + return room.MxRoomID, nil +} + +func dbGetMxPmRoom(protocol string, them connector.UserID, themMxId string, usMxId string, usAccount string) (string, error) { + slot_key := fmt.Sprintf("pmroom: %s / %s / %s / %s", protocol, usMxId, usAccount, them) + dbLockSlot(slot_key) + defer dbUnlockSlot(slot_key) + + var room DbPmRoomMap + + must_create := db.First(&room, DbPmRoomMap{ + MxUserID: usMxId, + Protocol: protocol, + AccountName: usAccount, + UserID: them, + }).RecordNotFound() + if must_create { + name := fmt.Sprintf("%s (%s)", them, protocol) + + mx_room_id, err := mx.CreateDirectRoomAs([]string{usMxId}, themMxId) + if err != nil { + log.Printf("Could not create room for %s: %s", name, err) + return "", err + } + + //err = mxRoomJoinAs(mx_room_id, themMxId) + //if err != nil { + // log.Printf("Could not join %s as %s", mx_room_id, themMxId) + // return "", err + //} + + room = DbPmRoomMap{ + MxUserID: usMxId, + Protocol: protocol, + AccountName: usAccount, + UserID: them, + MxRoomID: mx_room_id, + } + db.Create(&room) + } + log.Debugf("Got PM room id: %s", room.MxRoomID) + + return room.MxRoomID, nil +} + +func dbGetMxUser(protocol string, userId connector.UserID) (string, error) { + slot_key := fmt.Sprintf("user: %s / %s", protocol, userId) + dbLockSlot(slot_key) + defer dbUnlockSlot(slot_key) + + var user DbUserMap + + must_create := db.First(&user, DbUserMap{ + Protocol: protocol, + UserID: userId, + }).RecordNotFound() + if must_create { + username := userMxId(protocol, userId) + + err := mx.RegisterUser(username) + if err != nil { + if mxE, ok := err.(*mxlib.MxError); !ok || mxE.ErrCode != "M_USER_IN_USE" { + log.Printf("Could not register %s: %s", username, err) + return "", err + } + } + + mxid := fmt.Sprintf("@%s:%s", username, config.MatrixDomain) + mx.ProfileDisplayname(mxid, fmt.Sprintf("%s (%s)", userId, protocol)) + + user = DbUserMap{ + Protocol: protocol, + UserID: userId, + MxUserID: mxid, + } + db.Create(&user) + } + + return user.MxUserID, nil +} + +func dbIsPmRoom(mxRoomId string) *DbPmRoomMap { + var pm_room DbPmRoomMap + if db.First(&pm_room, DbPmRoomMap{MxRoomID: mxRoomId}).RecordNotFound() { + return nil + } else { + return &pm_room + } +} + +func dbIsPublicRoom(mxRoomId string) *DbRoomMap { + var room DbRoomMap + if db.First(&room, DbRoomMap{MxRoomID: mxRoomId}).RecordNotFound() { + return nil + } else { + return &room + } +} diff --git a/main.go b/main.go index 0864b66..d74e3d8 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,6 @@ import ( log "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" - "git.deuxfleurs.fr/Deuxfleurs/easybridge/appservice" "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/irc" "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/mattermost" @@ -166,15 +165,7 @@ func main() { reg_file := readRegistration(config.Registration) registration = ®_file - as_config := &appservice.Config{ - HttpBindAddr: config.HttpBindAddr, - Server: config.Server, - DbType: config.DbType, - DbPath: config.DbPath, - MatrixDomain: config.MatrixDomain, - } - - errch, err := appservice.Start(registration, as_config) + errch, err := StartAppService() if err != nil { log.Fatal(err) } @@ -192,7 +183,7 @@ func main() { default: log.Fatalf("Invalid protocol %s", params.Protocol) } - account := &appservice.Account{ + account := &Account{ MatrixUser: fmt.Sprintf("@%s:%s", user, config.MatrixDomain), AccountName: name, Protocol: params.Protocol, @@ -200,7 +191,7 @@ func main() { JoinedRooms: map[connector.RoomID]bool{}, } conn.SetHandler(account) - appservice.AddAccount(account) + AddAccount(account) go connectAndJoin(account, params) } } @@ -211,7 +202,7 @@ func main() { } } -func connectAndJoin(account *appservice.Account, params ConfigAccount) { +func connectAndJoin(account *Account, params ConfigAccount) { log.Printf("Connecting to %s", params.Protocol) err := account.Conn.Configure(params.Config) if err != nil { diff --git a/server.go b/server.go new file mode 100644 index 0000000..10721be --- /dev/null +++ b/server.go @@ -0,0 +1,259 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" + + "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" + "git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib" +) + +type Config struct { + HttpBindAddr string + Server string + DbType string + DbPath string + MatrixDomain string +} + +var mx *mxlib.Client + +func StartAppService() (chan error, error) { + mx = mxlib.NewClient(config.Server, registration.AsToken) + + err := InitDb() + if err != nil { + return nil, err + } + + err = mx.RegisterUser(registration.SenderLocalpart) + if mxe, ok := err.(*mxlib.MxError); !ok || mxe.ErrCode != "M_USER_IN_USE" { + return nil, err + } + if err == nil { + // If Easybridge account was created, update avatar and display name + err = mx.ProfileAvatar(ezbrMxId(), &connector.FileMediaObject{ + Path: "easybridge.jpg", + }) + if err != nil { + return nil, err + } + err = mx.ProfileDisplayname(ezbrMxId(), fmt.Sprintf("Easybridge (%s)", EASYBRIDGE_SYSTEM_PROTOCOL)) + if err != nil { + return nil, err + } + } + + router := mux.NewRouter() + router.HandleFunc("/_matrix/app/v1/transactions/{txnId}", handleTxn) + router.HandleFunc("/transactions/{txnId}", handleTxn) + + errch := make(chan error) + go func() { + log.Printf("Starting HTTP server on %s", config.HttpBindAddr) + err := http.ListenAndServe(config.HttpBindAddr, checkTokenAndLog(router)) + if err != nil { + errch <- err + } + }() + + return errch, nil +} + +func checkTokenAndLog(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + if strings.Join(r.Form["access_token"], "") != registration.HsToken { + http.Error(w, "Wrong or no token provided", http.StatusUnauthorized) + return + } + + log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) + handler.ServeHTTP(w, r) + }) +} + +func handleTxn(w http.ResponseWriter, r *http.Request) { + if r.Method == "PUT" { + var txn mxlib.Transaction + err := json.NewDecoder(r.Body).Decode(&txn) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + log.Warnf("JSON decode error: %s\n", err) + return + } + + log.Debugf("Got transaction %#v\n", txn) + + for i := range txn.Events { + ev := &txn.Events[i] + if strings.HasPrefix(ev.Sender, "@"+registration.SenderLocalpart) { + // Don't do anything with ezbr events that come back to us + continue + } + err = handleTxnEvent(ev) + if err != nil { + ezbrSystemSend(ev.Sender, fmt.Sprintf("Could not process %s (%s): %s", ev.Type, ev.Sender, err)) + } + } + + fmt.Fprintf(w, "{}\n") + } else { + http.Error(w, "Expected PUT request", http.StatusBadRequest) + } +} + +func handleTxnEvent(e *mxlib.Event) error { + if e.Type == "m.room.message" { + ev := &connector.Event{ + Type: connector.EVENT_MESSAGE, + Text: e.Content["body"].(string), + } + typ := e.Content["msgtype"].(string) + if typ == "m.emote" { + ev.Type = connector.EVENT_MESSAGE + } else if typ == "m.file" || typ == "m.image" { + ev.Text = "" + ev.Attachments = []connector.MediaObject{mx.ParseMediaInfo(e.Content)} + } + + if pm_room := dbIsPmRoom(e.RoomId); pm_room != nil { + if pm_room.Protocol == EASYBRIDGE_SYSTEM_PROTOCOL { + handleSystemMessage(e.Sender, e.Content["body"].(string)) + return nil + } + // If this is a private message room + acct := FindAccount(pm_room.MxUserID, pm_room.AccountName) + if acct == nil { + return fmt.Errorf("Not connected to %s", pm_room.AccountName) + } else if e.Sender == pm_room.MxUserID { + ev.Author = acct.Conn.User() + ev.Recipient = pm_room.UserID + return acct.Conn.Send(ev) + } + } else if room := dbIsPublicRoom(e.RoomId); room != nil { + // If this is a regular room + acct := FindJoinedAccount(e.Sender, room.Protocol, room.RoomID) + if acct != nil { + ev.Author = acct.Conn.User() + ev.Room = room.RoomID + return acct.Conn.Send(ev) + } else { + mx.RoomKick(e.RoomId, e.Sender, fmt.Sprintf("Not present in %s on %s, please talk with Easybridge to rejoin", room.RoomID, room.Protocol)) + return fmt.Errorf("not joined %s on %s", room.RoomID, room.Protocol) + } + } else { + return fmt.Errorf("Room not bridged") + } + } else if e.Type == "m.room.member" { + ms := e.Content["membership"].(string) + if ms == "leave" { + if pm_room := dbIsPmRoom(e.RoomId); pm_room != nil { + // If leaving a PM room, we must delete it + them_mx := userMxId(pm_room.Protocol, pm_room.UserID) + mx.RoomLeaveAs(e.RoomId, them_mx) + db.Delete(pm_room) + return nil + } else if room := dbIsPublicRoom(e.RoomId); room != nil { + // If leaving a public room, leave from server as well + acct := FindJoinedAccount(e.Sender, room.Protocol, room.RoomID) + if acct != nil { + acct.Conn.Leave(room.RoomID) + return nil + // TODO: manage autojoin list, remove this room + } else { + mx.RoomKick(e.RoomId, e.Sender, fmt.Sprintf("Not present in %s on %s, please talk with Easybridge to rejoin", room.RoomID, room.Protocol)) + return fmt.Errorf("not joined %s on %s", room.RoomID, room.Protocol) + } + } else { + return fmt.Errorf("Room not bridged") + } + } + } else if e.Type == "m.room.topic" { + if room := dbIsPublicRoom(e.RoomId); room != nil { + acct := FindJoinedAccount(e.Sender, room.Protocol, room.RoomID) + if acct != nil { + return acct.Conn.SetRoomInfo(room.RoomID, &connector.RoomInfo{ + Topic: e.Content["topic"].(string), + }) + } else { + return fmt.Errorf("Could not find room account for %s %s %s", e.Sender, room.Protocol, room.RoomID) + } + } + } + return nil +} + +func handleSystemMessage(mxid string, msg string) { + cmd := strings.Fields(msg) + switch cmd[0] { + case "help": + ezbrSystemSend(mxid, "Welcome to Easybridge! Here is a list of available commands:") + ezbrSystemSend(mxid, "- help: request help") + ezbrSystemSend(mxid, "- list: list accounts") + ezbrSystemSend(mxid, "- accounts: list accounts") + ezbrSystemSend(mxid, "- join : join public chat room") + ezbrSystemSend(mxid, "- talk : open private conversation to contact") + case "list", "account", "accounts": + one := false + if accts, ok := registeredAccounts[mxid]; ok { + for name, acct := range accts { + one = true + ezbrSystemSendf(mxid, "- %s (%s)", name, acct.Protocol) + } + } + if !one { + ezbrSystemSendf(mxid, "No account currently configured") + } + case "join": + account := findAccount(mxid, cmd[1]) + if account != nil { + err := account.Conn.Join(connector.RoomID(cmd[2])) + if err != nil { + ezbrSystemSendf(mxid, "%s", err) + } + } else { + ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1]) + } + case "talk": + account := findAccount(mxid, cmd[1]) + if account != nil { + quser := connector.UserID(cmd[2]) + err := account.Conn.Invite(quser, connector.RoomID("")) + if err != nil { + ezbrSystemSendf(mxid, "%s", err) + return + } + + quser_mxid, err := dbGetMxUser(account.Protocol, quser) + if err != nil { + ezbrSystemSendf(mxid, "%s", err) + return + } + _, err = dbGetMxPmRoom(account.Protocol, quser, quser_mxid, mxid, account.AccountName) + if err != nil { + ezbrSystemSendf(mxid, "%s", err) + } + } else { + ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1]) + } + default: + ezbrSystemSend(mxid, "Unrecognized command. Type `help` if you need some help!") + } +} + +func findAccount(mxid string, q string) *Account { + if accts, ok := registeredAccounts[mxid]; ok { + for name, acct := range accts { + if name == q || acct.Protocol == q { + return acct + } + } + } + return nil +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..c811a1e --- /dev/null +++ b/util.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "unicode" + + log "github.com/sirupsen/logrus" + + . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" +) + +const EASYBRIDGE_SYSTEM_PROTOCOL string = "✯◡✯" + +func ezbrMxId() string { + return fmt.Sprintf("@%s:%s", registration.SenderLocalpart, config.MatrixDomain) +} + +func ezbrSystemRoom(user_mx_id string) (string, error) { + return dbGetMxPmRoom(EASYBRIDGE_SYSTEM_PROTOCOL, UserID("Easybridge"), ezbrMxId(), user_mx_id, "easybridge") +} + +func ezbrSystemSend(user_mx_id string, msg string) { + mx_room_id, err := ezbrSystemRoom(user_mx_id) + if err == nil { + err = mx.SendMessageAs(mx_room_id, "m.text", msg, ezbrMxId()) + } + if err != nil { + log.Warnf("(%s) %s", user_mx_id, msg) + } +} + +func ezbrSystemSendf(user_mx_id string, format string, args ...interface{}) { + ezbrSystemSend(user_mx_id, fmt.Sprintf(format, args...)) +} + +// ---- + +func roomAlias(protocol string, id RoomID) string { + return fmt.Sprintf("_ezbr__%s__%s", safeStringForId(string(id)), protocol) +} + +func userMxId(protocol string, id UserID) string { + return fmt.Sprintf("_ezbr__%s__%s", safeStringForId(string(id)), protocol) +} + +func safeStringForId(in string) string { + id2 := "" + for _, c := range in { + if c == '@' { + id2 += "__" + } else if c == ':' { + id2 += "_" + } else if unicode.IsDigit(c) || unicode.IsLetter(c) { + id2 += string(c) + } + } + return id2 +} -- cgit v1.2.3