aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2020-02-16 22:07:41 +0100
committerAlex Auvolat <alex@adnab.me>2020-02-16 22:07:41 +0100
commitd2ccd6763a8a8a88e5cdbf95fd665e679f8e187e (patch)
tree8d17148b9424019c38d0d19f1b0e2ccef96b87e0
parent046ec6380b7bb363e537ade7fd254b5505dde32d (diff)
downloadeasybridge-d2ccd6763a8a8a88e5cdbf95fd665e679f8e187e.tar.gz
easybridge-d2ccd6763a8a8a88e5cdbf95fd665e679f8e187e.zip
Begin some bridging
-rw-r--r--appservice/account.go148
-rw-r--r--appservice/db.go50
-rw-r--r--appservice/matrix.go192
-rw-r--r--appservice/names.go21
-rw-r--r--appservice/server.go4
-rw-r--r--connector/irc/irc.go4
-rw-r--r--main.go5
-rw-r--r--mxlib/api.go61
8 files changed, 479 insertions, 6 deletions
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"`
+}