Begin some bridging

This commit is contained in:
Alex 2020-02-16 22:07:41 +01:00
parent 046ec6380b
commit d2ccd6763a
8 changed files with 479 additions and 6 deletions

View file

@ -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
}

View file

@ -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"`
}

192
appservice/matrix.go Normal file
View file

@ -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
}

21
appservice/names.go Normal file
View file

@ -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)
}

View file

@ -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")
}

View file

@ -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,
})

View file

@ -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,

View file

@ -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"`
}