An easy to set-up simple pupetting bridge for talking to all your IM contacts through Matrix
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

362 lines
10 KiB

package main
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"strings"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
"git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib"
)
var mx *mxlib.Client
func StartAppService(errch chan error, ctx context.Context) (*http.Server, error) {
mx = mxlib.NewClient(config.Server, registration.AsToken)
err := InitDb()
if err != nil {
return nil, err
}
if dbKvGet("ezbr_initialized") != "yes" {
err = mx.RegisterUser(registration.SenderLocalpart)
if mxe, ok := err.(*mxlib.MxError); !ok || mxe.ErrCode != "M_USER_IN_USE" {
return nil, err
}
_, st := os.Stat(config.AvatarFile)
if !os.IsNotExist(st) {
err = mx.ProfileAvatar(ezbrMxId(), &connector.FileMediaObject{
Path: config.AvatarFile,
})
if err != nil {
return nil, err
}
}
err = mx.ProfileDisplayname(ezbrMxId(), fmt.Sprintf("Easybridge (%s)", EASYBRIDGE_SYSTEM_PROTOCOL))
if err != nil {
return nil, err
}
dbKvPut("ezbr_initialized", "yes")
}
router := mux.NewRouter()
router.HandleFunc("/_matrix/app/v1/transactions/{txnId}", handleTxn)
router.HandleFunc("/transactions/{txnId}", handleTxn)
log.Printf("Starting HTTP server on %s", config.ASBindAddr)
http_server := &http.Server{
Addr: config.ASBindAddr,
Handler: checkTokenAndLog(router),
BaseContext: func(net.Listener) context.Context {
return ctx
},
}
go func() {
err := http_server.ListenAndServe()
if err != nil {
errch <- err
}
}()
// Notify users that Easybridge has restarted
go func() {
var users []DbAccountConfig
db.Model(&DbAccountConfig{}).Select("mx_user_id").Group("mx_user_id").Find(&users)
for _, u := range users {
ezbrSystemSendf(u.MxUserID,
"Easybridge has restarted, please visit %s or open configuration widget to reconnect to your accounts.",
config.WebURL)
}
}()
return http_server, 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, strings.Split(r.URL.String(), "?")[0])
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("Processing transaction %s (%d events)", mux.Vars(r)["txnId"], len(txn.Events))
log.Tracef("Transaction content: %#v\n", txn)
for i := range txn.Events {
ev := &txn.Events[i]
if isBridgedIdentifier(ev.Sender) {
// 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 (from %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" {
e_body, ok := e.Content["body"].(string)
if !ok {
return fmt.Errorf("Invalid m.room.message event, body is not defined: %#v", e)
}
typ, ok := e.Content["msgtype"].(string)
if !ok {
return fmt.Errorf("Invalid m.room.message event, msgtype is not defined: %#v", e)
}
ev := &connector.Event{
Type: connector.EVENT_MESSAGE,
Text: e_body,
Id: e.EventId,
}
if typ == "m.emote" {
ev.Type = connector.EVENT_MESSAGE
} else if typ == "m.file" || typ == "m.image" {
ev.Text = ""
ev.Attachments = []connector.SMediaObject{
connector.SMediaObject{mx.ParseMediaInfo(e.Content)},
}
}
if pm_room := dbIsPmRoom(e.RoomId); pm_room != nil {
if pm_room.Protocol == EASYBRIDGE_SYSTEM_PROTOCOL {
handleSystemMessage(e.Sender, e_body)
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
_, err := acct.Conn.Send(ev)
return err
}
} 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 {
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 {
ev.Author = acct.Conn.User()
ev.Room = room.RoomID
// use room id as lock slot key, see account.go in eventInternal
dbLockSlot(e.RoomId)
defer dbUnlockSlot(e.RoomId)
created_ev_id, err := acct.Conn.Send(ev)
if err == nil && created_ev_id != "" {
cache_key := fmt.Sprintf("%s/event_seen/%s/%s",
room.Protocol, e.RoomId, created_ev_id)
dbKvPutLocked(cache_key, "yes")
}
return err
}
} else {
return fmt.Errorf("Room not bridged")
}
} else if e.Type == "m.room.member" {
ms, ok := e.Content["membership"].(string)
if !ok {
return fmt.Errorf("Invalid m.room.member event, membership is not defined: %#v", e)
}
if ms == "leave" {
if pm_room := dbIsPmRoom(e.RoomId); pm_room != nil {
// If user leaves a PM room, we must drop it
dbDeletePmRoom(pm_room)
them_mx, err := dbGetMxUser(pm_room.Protocol, pm_room.UserID)
if err != nil {
return err
}
mx.RoomLeaveAs(e.RoomId, them_mx)
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)
acct.delAutojoin(room.RoomID)
return nil
} 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" {
e_topic, ok := e.Content["topic"].(string)
if !ok {
return fmt.Errorf("Invalid m.room.topic event, topic is not defined: %#v", e)
}
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_topic,
})
} 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)
if len(cmd) == 0 {
return
}
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 <protocol or account> <room id>: join public chat room")
ezbrSystemSend(mxid, "- talk <protocol or account> <user id>: open private conversation to contact")
ezbrSystemSend(mxid, "- search <protocol or account> <name>: search for users by name")
ezbrSystemSend(mxid, "- cmd <protocol or account> <command>: send special command to account")
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":
if len(cmd) != 3 {
ezbrSystemSendf(mxid, "Usage: %s <protocol or account> <room id>", cmd[0])
return
}
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 "query", "talk":
if len(cmd) != 3 {
ezbrSystemSendf(mxid, "Usage: %s <protocol or account> <user id>", cmd[0])
return
}
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])
}
case "search":
if len(cmd) < 3 {
ezbrSystemSendf(mxid, "Usage: %s <protocol or account> <name>", cmd[0])
return
}
account := findAccount(mxid, cmd[1])
if account != nil {
rep, err := account.Conn.SearchForUsers(strings.Join(cmd[2:], " "))
if err != nil {
ezbrSystemSendf(mxid, "Search error: %s", err)
} else {
ezbrSystemSendf(mxid, "%d users found", len(rep))
for _, user := range rep {
ezbrSystemSendf(mxid, "- %s (%s)", user.DisplayName, user.ID)
}
}
} else {
ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1])
}
case "cmd":
if len(cmd) < 3 {
ezbrSystemSendf(mxid, "Usage: %s <protocol or account> <name>", cmd[0])
return
}
account := findAccount(mxid, cmd[1])
if account != nil {
account.Conn.UserCommand(strings.Join(cmd[2:], " "))
} 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 strings.EqualFold(name, q) || strings.EqualFold(acct.Protocol, q) {
return acct
}
}
}
return nil
}