From d2ccd6763a8a8a88e5cdbf95fd665e679f8e187e Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Sun, 16 Feb 2020 22:07:41 +0100 Subject: [PATCH] Begin some bridging --- appservice/account.go | 148 +++++++++++++++++++++++++++++++- appservice/db.go | 50 +++++++++++ appservice/matrix.go | 192 ++++++++++++++++++++++++++++++++++++++++++ appservice/names.go | 21 +++++ appservice/server.go | 4 +- connector/irc/irc.go | 4 +- main.go | 5 +- mxlib/api.go | 61 ++++++++++++++ 8 files changed, 479 insertions(+), 6 deletions(-) create mode 100644 appservice/matrix.go create mode 100644 appservice/names.go diff --git a/appservice/account.go b/appservice/account.go index 533e01e..4316362 100644 --- a/appservice/account.go +++ b/appservice/account.go @@ -1,6 +1,10 @@ package appservice import ( + "fmt" + "log" + + "git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib" . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" ) @@ -12,11 +16,31 @@ type Account struct { } func (a *Account) Joined(roomId RoomID) { - // TODO + mx_room_id, err := dbGetMxRoom(a.Protocol, roomId) + if err != nil { + return + } + + log.Printf("Joined %s (%s)\n", roomId, a.MatrixUser) + + err = mxRoomInvite(mx_room_id, a.MatrixUser) + if err != nil { + log.Printf("Could not invite %s to %s", a.MatrixUser, mx_room_id) + } } func (a *Account) Left(roomId RoomID) { - // TODO + mx_room_id, err := dbGetMxRoom(a.Protocol, roomId) + if err != nil { + return + } + + log.Printf("Joined %s (%s)\n", roomId, a.MatrixUser) + + err = mxRoomKick(mx_room_id, a.MatrixUser, fmt.Sprintf("got leave room event on %s", a.Protocol)) + if err != nil { + log.Printf("Could not invite %s to %s", a.MatrixUser, mx_room_id) + } } func (a *Account) UserInfoUpdated(user UserID, info *UserInfo) { @@ -28,5 +52,125 @@ func (a *Account) RoomInfoUpdated(roomId RoomID, info *RoomInfo) { } func (a *Account) Event(event *Event) { + mx_user_id, err := dbGetMxUser(a.Protocol, event.Author) + if err != nil { + return + } + + 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 = mxRoomInvite(mx_room_id, mx_user_id) + if err != nil { + log.Printf("Could not invite %s to %s", a.MatrixUser, mx_room_id) + } + + err = mxRoomJoinAs(mx_room_id, mx_user_id) + if err != nil { + log.Printf("Could not join %s as %s", a.MatrixUser, mx_room_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 = mxRoomLeaveAs(mx_room_id, mx_user_id) + if err != nil { + log.Printf("Could not leave %s as %s", a.MatrixUser, mx_room_id) + } + } else if event.Type == EVENT_MESSAGE { + if len(event.Room) > 0 { + log.Printf("%s msg %s %s", a.Protocol, event.Author, event.Room) + mx_room_id, err := dbGetMxRoom(a.Protocol, event.Room) + if err != nil { + return + } + + err = mxSendMessageAs(mx_room_id, event.Text, mx_user_id) + if err != nil { + log.Printf("Could not send %s as %s", event.Text, mx_user_id) + } + } else { + // TODO + } + } // TODO } + +// ---- + +func dbGetMxRoom(protocol string, roomId RoomID) (string, error) { + 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 := mxDirectoryRoom(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 = mxCreateRoom(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.Printf("Got room id: %s", room.MxRoomID) + + return room.MxRoomID, nil +} + +func dbGetMxUser(protocol string, userId UserID) (string, error) { + var user DbUserMap + + must_create := db.First(&user, DbUserMap{ + Protocol: protocol, + UserID: userId, + }).RecordNotFound() + if must_create { + username := userMxId(protocol, userId) + + err := mxRegisterUser(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) + mxProfileDisplayname(mxid, fmt.Sprintf("%s (%s)", userId, protocol)) + + user = DbUserMap{ + Protocol: protocol, + UserID: userId, + MxUserID: mxid, + } + db.Create(&user) + } + + return user.MxUserID, nil +} + diff --git a/appservice/db.go b/appservice/db.go index 2c71312..f8cbccf 100644 --- a/appservice/db.go +++ b/appservice/db.go @@ -1,6 +1,7 @@ package appservice import ( + "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/postgres" @@ -17,5 +18,54 @@ func InitDb() error { return err } + 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 } + +// 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.RoomID + + // Bridged room for PMs + MxRoomID string `gorm:"index:mxroomoid"` +} + diff --git a/appservice/matrix.go b/appservice/matrix.go new file mode 100644 index 0000000..96f643a --- /dev/null +++ b/appservice/matrix.go @@ -0,0 +1,192 @@ +package appservice + +import ( + "fmt" + "net/url" + "log" + "net/http" + "time" + "bytes" + "encoding/json" + + . "git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib" +) + +var httpClient *http.Client + +func init() { + tr := &http.Transport{ + MaxIdleConns: 10, + IdleConnTimeout: 30 * time.Second, + DisableCompression: true, + } + httpClient = &http.Client{Transport: tr} +} + +func mxGetApiCall(endpoint string, response interface{}) error { + log.Printf("Matrix GET request: %s\n", endpoint) + + req, err := http.NewRequest("GET", config.Server + endpoint, nil) + if err != nil { + return err + } + + return mxDoAndParse(req, response) +} + +func mxPutApiCall(endpoint string, data interface{}, response interface{}) error { + body, err := json.Marshal(data) + if err != nil { + return err + } + + log.Printf("Matrix PUT request: %s %s\n", endpoint, string(body)) + + req, err := http.NewRequest("PUT", config.Server + endpoint, bytes.NewBuffer(body)) + if err != nil { + return err + } + req.Header.Add("Content-Type", "application/json") + + return mxDoAndParse(req, response) +} + +func mxPostApiCall(endpoint string, data interface{}, response interface{}) error { + body, err := json.Marshal(data) + if err != nil { + return err + } + + log.Printf("Matrix POST request: %s %s\n", endpoint, string(body)) + + req, err := http.NewRequest("POST", config.Server + endpoint, bytes.NewBuffer(body)) + if err != nil { + return err + } + req.Header.Add("Content-Type", "application/json") + + return mxDoAndParse(req, response) +} + +func mxDoAndParse(req *http.Request, response interface{}) error { + req.Header.Add("Authorization", "Bearer " + registration.AsToken) + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + var e MxError + err = json.NewDecoder(resp.Body).Decode(&e) + if err != nil { + return err + } + log.Printf("Response (%d): %#v\n", resp.StatusCode, e) + return &e + } + + err = json.NewDecoder(resp.Body).Decode(response) + if err != nil { + return err + } + + log.Printf("Response: %#v\n", response) + return nil +} + +// ---- + +func mxRegisterUser(username string) error { + req := RegisterRequest{ + Username: username, + } + var rep RegisterResponse + return mxPostApiCall("/_matrix/client/r0/register?kind=user", &req, &rep) +} + +func mxProfileDisplayname(userid string, displayname string) error { + req := ProfileDisplaynameRequest{ + Displayname: displayname, + } + var rep struct{} + err := mxPutApiCall(fmt.Sprintf("/_matrix/client/r0/profile/%s/displayname?user_id=%s", + url.QueryEscape(userid), url.QueryEscape(userid)), + &req, &rep) + return err +} + +func mxDirectoryRoom(alias string) (string, error) { + var rep DirectoryRoomResponse + err := mxGetApiCall("/_matrix/client/r0/directory/room/" + url.QueryEscape(alias), &rep) + if err != nil { + return "", err + } + return rep.RoomId, nil +} + +func mxCreateRoom(name string, alias string, invite []string) (string, error) { + rq := CreateRoomRequest{ + Preset: "private_chat", + RoomAliasName: alias, + Name: name, + Topic: "", + Invite: invite, + CreationContent: map[string]interface{} { + "m.federate": false, + }, + } + var rep CreateRoomResponse + err := mxPostApiCall("/_matrix/client/r0/createRoom", &rq, &rep) + if err != nil { + return "", err + } + return rep.RoomId, nil +} + +func mxRoomInvite(room string, user string) error { + rq := RoomInviteRequest{ + UserId: user, + } + var rep struct{} + err := mxPostApiCall("/_matrix/client/r0/rooms/" + url.QueryEscape(room) + "/invite", &rq, &rep) + return err +} + +func mxRoomKick(room string, user string, reason string) error { + rq := RoomKickRequest{ + UserId: user, + Reason: reason, + } + var rep struct{} + err := mxPostApiCall("/_matrix/client/r0/rooms/" + url.QueryEscape(room) + "/kick", &rq, &rep) + return err +} + +func mxRoomJoinAs(room string, user string) error { + rq := struct{}{} + var rep RoomJoinResponse + err := mxPostApiCall("/_matrix/client/r0/rooms/" + url.QueryEscape(room) + "/join?user_id=" + url.QueryEscape(user), &rq, &rep) + return err +} + +func mxRoomLeaveAs(room string, user string) error { + rq := struct{}{} + var rep struct{} + err := mxPostApiCall("/_matrix/client/r0/rooms/" + url.QueryEscape(room) + "/leave?user_id=" + url.QueryEscape(user), &rq, &rep) + return err +} + +func mxSendMessageAs(room string, body string, user string) error { + txn_id := time.Now().UnixNano() + rq := RoomSendRequest{ + MsgType: "m.text", + Body: body, + } + var rep RoomSendResponse + err := mxPutApiCall(fmt.Sprintf( + "/_matrix/client/r0/rooms/%s/send/m.room.message/%d?user_id=%s", + url.QueryEscape(room), txn_id, url.QueryEscape(user)), + &rq, &rep) + return err +} diff --git a/appservice/names.go b/appservice/names.go new file mode 100644 index 0000000..4a5d186 --- /dev/null +++ b/appservice/names.go @@ -0,0 +1,21 @@ +package appservice + +import ( + "fmt" + "strings" + + . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" +) + +func roomAlias(protocol string, id RoomID) string { + id2 := strings.ReplaceAll(string(id), "#", "") + id2 = strings.ReplaceAll(id2, "@", "__") + + return fmt.Sprintf("_ezbr__%s__%s", id2, protocol) +} + +func userMxId(protocol string, id UserID) string { + id2 := strings.ReplaceAll(string(id), "@", "__") + + return fmt.Sprintf("_ezbr__%s__%s", id2, protocol) +} diff --git a/appservice/server.go b/appservice/server.go index 395d383..8e4c263 100644 --- a/appservice/server.go +++ b/appservice/server.go @@ -16,6 +16,7 @@ type Config struct { Server string DbType string DbPath string + MatrixDomain string } @@ -33,6 +34,7 @@ func Start(r *mxlib.Registration, c *Config) (chan error, error) { router := mux.NewRouter() router.HandleFunc("/_matrix/app/v1/transactions/{txnId}", handleTxn) + router.HandleFunc("/transactions/{txnId}", handleTxn) errch := make(chan error) go func() { @@ -68,5 +70,5 @@ func handleTxn(w http.ResponseWriter, r *http.Request) { log.Printf("Got transaction %#v\n", txn) - fmt.Fprintf(w, "{}") + fmt.Fprintf(w, "{}\n") } diff --git a/connector/irc/irc.go b/connector/irc/irc.go index 396b665..e57d71b 100644 --- a/connector/irc/irc.go +++ b/connector/irc/irc.go @@ -2,7 +2,7 @@ package irc import ( "time" - "os" + _ "os" "strings" "fmt" @@ -67,7 +67,7 @@ func (irc *IRC) Configure(c Configuration) error { Port: port, Nick: irc.nick, User: irc.nick, - Out: os.Stderr, + //Out: os.Stderr, SSL: ssl, }) diff --git a/main.go b/main.go index f7a3e75..0f8f3f0 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "crypto/rand" "encoding/hex" "flag" @@ -32,6 +33,7 @@ type ConfigFile struct { Server string `json:"homeserver_url"` DbType string `json:"db_type"` DbPath string `json:"db_path"` + MatrixDomain string `json:"matrix_domain"` Accounts map[string]map[string]ConfigAccount `json:"accounts"` } @@ -164,6 +166,7 @@ func main() { Server: config.Server, DbType: config.DbType, DbPath: config.DbPath, + MatrixDomain: config.MatrixDomain, } errch, err := appservice.Start(registration, as_config) @@ -181,7 +184,7 @@ func main() { conn = &xmpp.XMPP{} } account := &appservice.Account{ - MatrixUser: user, + MatrixUser: fmt.Sprintf("@%s:%s", user, config.MatrixDomain), AccountName: name, Protocol: params.Protocol, Conn: conn, diff --git a/mxlib/api.go b/mxlib/api.go index 59e1267..cb2d10c 100644 --- a/mxlib/api.go +++ b/mxlib/api.go @@ -4,6 +4,15 @@ import ( _ "encoding/json" ) +type MxError struct { + ErrCode string `json:"errcode"` + ErrMsg string `json:"error"` +} + +func (e *MxError) Error() string { + return e.ErrMsg +} + type Transaction struct { Events []Event `json:"events"` } @@ -16,3 +25,55 @@ type Event struct { Sender string `json:"sender"` OriginServerTs int `json:"origin_server_ts"` } + +type RegisterRequest struct { + Username string `json:"username"` +} + +type RegisterResponse struct { + UserId string `json:"user_id"` + AccessToken string `json:"access_token"` + DeviceId string `json:"device_id"` +} + +type ProfileDisplaynameRequest struct { + Displayname string `json:"displayname"` +} + +type CreateRoomRequest struct { + Preset string `json:"preset"` + RoomAliasName string `json:"room_alias_name"` + Name string `json:"name"` + Topic string `json:"topic"` + Invite []string `json:"invite"` + CreationContent map[string]interface{} `json:"creation_content"` +} + +type CreateRoomResponse struct { + RoomId string `json:"room_id"` +} + +type DirectoryRoomResponse struct { + RoomId string `json:"room_id"` + Servers []string `json:"string"` +} + +type RoomInviteRequest struct { + UserId string `json:"user_id"` +} + +type RoomKickRequest struct { + UserId string `json:"user_id"` + Reason string `json:"reason"` +} +type RoomJoinResponse struct { + RoomId string `json:"room_id"` +} + +type RoomSendRequest struct { + MsgType string `json:"msgtype"` + Body string `json:"body"` +} +type RoomSendResponse struct { + EventId string `json:"event_id"` +}