From a9124b89bd4456bfc674149cc9b0e5d34180d1dc Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 18 Aug 2018 22:57:08 +0300 Subject: [PATCH] Fix and add things * Fix user ID reservation in registration * Fix some database things * Add commands * Add basic contact syncing and portal creation * Add better error logging --- commands.go | 104 +++++++++++++++++++++++++++ config/bridge.go | 19 +++-- config/registration.go | 2 +- database/database.go | 18 +++-- database/portal.go | 17 ++++- database/puppet.go | 14 +++- database/user.go | 18 +++-- example-config.yaml | 10 +-- main.go | 29 +++++--- matrix.go | 75 +++++++++++++++----- portal.go | 59 ++++++++++++++-- puppet.go | 33 +++++++-- user.go | 155 +++++++++++++++++++++++++++++++---------- 13 files changed, 455 insertions(+), 98 deletions(-) create mode 100644 commands.go diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..dd7b7d0 --- /dev/null +++ b/commands.go @@ -0,0 +1,104 @@ +// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "maunium.net/go/mautrix-whatsapp/types" + "strings" + "maunium.net/go/mautrix-appservice" + "maunium.net/go/maulogger" +) + +type CommandHandler struct { + bridge *Bridge + log maulogger.Logger +} + +func NewCommandHandler(bridge *Bridge) *CommandHandler { + return &CommandHandler{ + bridge: bridge, + log: bridge.Log.Sub("Command handler"), + } +} + +type CommandEvent struct { + Bot *appservice.IntentAPI + Bridge *Bridge + Handler *CommandHandler + RoomID types.MatrixRoomID + User *User + Args []string +} + +func (ce *CommandEvent) Reply(msg string) { + _, err := ce.Bot.SendNotice(string(ce.RoomID), msg) + if err != nil { + ce.Handler.log.Warnfln("Failed to reply to command from %s: %v", ce.User.ID, err) + } +} + +func (handler *CommandHandler) Handle(roomID types.MatrixRoomID, user *User, message string) { + args := strings.Split(message, " ") + cmd := strings.ToLower(args[0]) + ce := &CommandEvent{ + Bot: handler.bridge.AppService.BotIntent(), + Bridge: handler.bridge, + Handler: handler, + RoomID: roomID, + User: user, + Args: args[1:], + } + switch cmd { + case "login": + handler.CommandLogin(ce) + case "logout": + handler.CommandLogout(ce) + case "help": + handler.CommandHelp(ce) + } +} + +func (handler *CommandHandler) CommandLogin(ce *CommandEvent) { + if ce.User.Session != nil { + ce.Reply("You're already logged in.") + return + } + + ce.User.Connect(true) + ce.User.Login(ce.RoomID) +} + +func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { + if ce.User.Session == nil { + ce.Reply("You're not logged in.") + return + } + err := ce.User.Conn.Logout() + if err != nil { + ce.User.log.Warnln("Error while logging out:", err) + ce.Reply("Error while logging out (see logs for details)") + return + } + ce.User.Conn = nil + ce.User.Session = nil + ce.User.Update() + ce.Reply("Logged out successfully.") +} + +func (handler *CommandHandler) CommandHelp(ce *CommandEvent) { + ce.Reply("Help is not yet implemented 3:") +} diff --git a/config/bridge.go b/config/bridge.go index a6bd4c5..b9df1a1 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -22,6 +22,8 @@ import ( "maunium.net/go/mautrix-appservice" "strings" "strconv" + "github.com/Rhymen/go-whatsapp" + "maunium.net/go/mautrix-whatsapp/types" ) type BridgeConfig struct { @@ -62,16 +64,16 @@ type UsernameTemplateArgs struct { UserID string } -func (bc BridgeConfig) FormatDisplayname(displayname string) string { +func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) string { var buf bytes.Buffer - bc.displaynameTemplate.Execute(&buf, DisplaynameTemplateArgs{ - Displayname: displayname, - }) + bc.displaynameTemplate.Execute(&buf, contact) return buf.String() } -func (bc BridgeConfig) FormatUsername(receiver, userID string) string { +func (bc BridgeConfig) FormatUsername(receiver types.MatrixUserID, userID types.WhatsAppID) string { var buf bytes.Buffer + receiver = strings.Replace(receiver, "@", "=40", 1) + receiver = strings.Replace(receiver, ":", "=3", 1) bc.usernameTemplate.Execute(&buf, UsernameTemplateArgs{ Receiver: receiver, UserID: userID, @@ -80,7 +82,12 @@ func (bc BridgeConfig) FormatUsername(receiver, userID string) string { } func (bc BridgeConfig) MarshalYAML() (interface{}, error) { - bc.DisplaynameTemplate = bc.FormatDisplayname("{{.Displayname}}") + bc.DisplaynameTemplate = bc.FormatDisplayname(whatsapp.Contact{ + Jid: "{{.Jid}}", + Notify: "{{.Notify}}", + Name: "{{.Name}}", + Short: "{{.Short}}", + }) bc.UsernameTemplate = bc.FormatUsername("{{.Receiver}}", "{{.UserID}}") return bc, nil } diff --git a/config/registration.go b/config/registration.go index 25a8a78..2f68ee0 100644 --- a/config/registration.go +++ b/config/registration.go @@ -55,7 +55,7 @@ func (config *Config) copyToRegistration(registration *appservice.Registration) registration.SenderLocalpart = config.AppService.Bot.Username userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", - config.Bridge.FormatUsername("[0-9]+", "[0-9]+"), + config.Bridge.FormatUsername(".+", "[0-9]+"), config.Homeserver.Domain)) if err != nil { return err diff --git a/database/database.go b/database/database.go index d6f930b..c9be10f 100644 --- a/database/database.go +++ b/database/database.go @@ -56,10 +56,20 @@ func New(file string) (*Database, error) { return db, nil } -func (db *Database) CreateTables() { - db.User.CreateTable() - db.Portal.CreateTable() - db.Puppet.CreateTable() +func (db *Database) CreateTables() error { + err := db.User.CreateTable() + if err != nil { + return err + } + err = db.Portal.CreateTable() + if err != nil { + return err + } + err = db.Puppet.CreateTable() + if err != nil { + return err + } + return nil } type Scannable interface { diff --git a/database/portal.go b/database/portal.go index 8055ed3..766e64d 100644 --- a/database/portal.go +++ b/database/portal.go @@ -19,6 +19,7 @@ package database import ( log "maunium.net/go/maulogger" "maunium.net/go/mautrix-whatsapp/types" + "database/sql" ) type PortalQuery struct { @@ -33,7 +34,7 @@ func (pq *PortalQuery) CreateTable() error { mxid VARCHAR(255) NOT NULL UNIQUE, PRIMARY KEY (jid, owner), - FOREIGN KEY owner REFERENCES user(mxid) + FOREIGN KEY (owner) REFERENCES user(mxid) )`) return err } @@ -80,22 +81,34 @@ type Portal struct { JID types.WhatsAppID MXID types.MatrixRoomID Owner types.MatrixUserID + + Name string + Avatar string } func (portal *Portal) Scan(row Scannable) *Portal { err := row.Scan(&portal.JID, &portal.MXID, &portal.Owner) if err != nil { - portal.log.Fatalln("Database scan failed:", err) + if err != sql.ErrNoRows { + portal.log.Fatalln("Database scan failed:", err) + } + return nil } return portal } func (portal *Portal) Insert() error { _, err := portal.db.Exec("INSERT INTO portal VALUES (?, ?, ?)", portal.JID, portal.Owner, portal.MXID) + if err != nil { + portal.log.Warnfln("Failed to update %s->%s: %v", portal.JID, portal.Owner, err) + } return err } func (portal *Portal) Update() error { _, err := portal.db.Exec("UPDATE portal SET mxid=? WHERE jid=? AND owner=?", portal.MXID, portal.JID, portal.Owner) + if err != nil { + portal.log.Warnfln("Failed to update %s->%s: %v", portal.JID, portal.Owner, err) + } return err } diff --git a/database/puppet.go b/database/puppet.go index 8e44be3..dfb4bc0 100644 --- a/database/puppet.go +++ b/database/puppet.go @@ -19,6 +19,7 @@ package database import ( log "maunium.net/go/maulogger" "maunium.net/go/mautrix-whatsapp/types" + "database/sql" ) type PuppetQuery struct { @@ -59,7 +60,7 @@ func (pq *PuppetQuery) GetAll(receiver types.MatrixUserID) (puppets []*Puppet) { } func (pq *PuppetQuery) Get(jid types.WhatsAppID, receiver types.MatrixUserID) *Puppet { - row := pq.db.QueryRow("SELECT * FROM user WHERE jid=? AND receiver=?", jid, receiver) + row := pq.db.QueryRow("SELECT * FROM puppet WHERE jid=? AND receiver=?", jid, receiver) if row == nil { return nil } @@ -80,7 +81,10 @@ type Puppet struct { func (puppet *Puppet) Scan(row Scannable) *Puppet { err := row.Scan(&puppet.JID, &puppet.Receiver, &puppet.Displayname, &puppet.Avatar) if err != nil { - puppet.log.Fatalln("Database scan failed:", err) + if err != sql.ErrNoRows { + puppet.log.Fatalln("Database scan failed:", err) + } + return nil } return puppet } @@ -88,6 +92,9 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet { func (puppet *Puppet) Insert() error { _, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?, ?)", puppet.JID, puppet.Receiver, puppet.Displayname, puppet.Avatar) + if err != nil { + puppet.log.Errorln("Failed to insert %s->%s: %v", puppet.JID, puppet.Receiver, err) + } return err } @@ -95,5 +102,8 @@ func (puppet *Puppet) Update() error { _, err := puppet.db.Exec("UPDATE puppet SET displayname=?, avatar=? WHERE jid=? AND receiver=?", puppet.Displayname, puppet.Avatar, puppet.JID, puppet.Receiver) + if err != nil { + puppet.log.Errorln("Failed to update %s->%s: %v", puppet.JID, puppet.Receiver, err) + } return err } diff --git a/database/user.go b/database/user.go index 5e35d90..5f84655 100644 --- a/database/user.go +++ b/database/user.go @@ -20,6 +20,7 @@ import ( log "maunium.net/go/maulogger" "github.com/Rhymen/go-whatsapp" "maunium.net/go/mautrix-whatsapp/types" + "database/sql" ) type UserQuery struct { @@ -45,8 +46,8 @@ func (uq *UserQuery) CreateTable() error { func (uq *UserQuery) New() *User { return &User{ - db: uq.db, - log: uq.log, + db: uq.db, + log: uq.log, } } @@ -74,17 +75,20 @@ type User struct { db *Database log log.Logger - UserID types.MatrixUserID + ID types.MatrixUserID ManagementRoom types.MatrixRoomID Session *whatsapp.Session } func (user *User) Scan(row Scannable) *User { sess := whatsapp.Session{} - err := row.Scan(&user.UserID, &user.ManagementRoom, &sess.ClientId, &sess.ClientToken, &sess.ServerToken, + err := row.Scan(&user.ID, &user.ManagementRoom, &sess.ClientId, &sess.ClientToken, &sess.ServerToken, &sess.EncKey, &sess.MacKey, &sess.Wid) if err != nil { - user.log.Fatalln("Database scan failed:", err) + if err != sql.ErrNoRows { + user.log.Fatalln("Database scan failed:", err) + } + return nil } if len(sess.ClientId) > 0 { user.Session = &sess @@ -99,7 +103,7 @@ func (user *User) Insert() error { if user.Session != nil { sess = *user.Session } - _, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.UserID, user.ManagementRoom, + _, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.ID, user.ManagementRoom, sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid) return err } @@ -111,6 +115,6 @@ func (user *User) Update() error { } _, err := user.db.Exec("UPDATE user SET management_room=?, client_id=?, client_token=?, server_token=?, enc_key=?, mac_key=?, wid=? WHERE mxid=?", user.ManagementRoom, - sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid, user.UserID) + sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.Wid, user.ID) return err } diff --git a/example-config.yaml b/example-config.yaml index c90908b..505712b 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -43,12 +43,14 @@ appservice: # Bridge config. Currently unused. bridge: # Localpart template of MXIDs for WhatsApp users. - # {{.receiver}} is replaced with the WhatsApp user ID of the Matrix user receiving messages. - # {{.userid}} is replaced with the user ID of the WhatsApp user. + # {{.Receiver}} is replaced with the WhatsApp user ID of the Matrix user receiving messages. + # {{.UserID}} is replaced with the user ID of the WhatsApp user. username_template: "whatsapp_{{.Receiver}}_{{.UserID}}" # Displayname template for WhatsApp users. - # {{.displayname}} is replaced with the display name of the WhatsApp user. - displayname_template: "{{.Displayname}}" + # {{.Name}} - display name + # {{.Short}} - short display name (usually first name) + # {{.Notify}} - nickname (set by the target WhatsApp user) + displayname_template: "{{if .Name}}{{.Name}}{{else if .Notify}}{{.Notify}}{{else if .Short}}{{.Short}}{{else}}Unnamed user{{end}}" # The prefix for commands. Only required in non-management rooms. command_prefix: "!wa" diff --git a/main.go b/main.go index a7bae96..1381b9b 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,6 @@ import ( "maunium.net/go/mautrix-appservice" log "maunium.net/go/maulogger" "maunium.net/go/mautrix-whatsapp/database" - "maunium.net/go/gomatrix" "maunium.net/go/mautrix-whatsapp/types" ) @@ -60,18 +59,21 @@ func (bridge *Bridge) GenerateRegistration() { type Bridge struct { AppService *appservice.AppService EventProcessor *appservice.EventProcessor + MatrixHandler *MatrixHandler Config *config.Config DB *database.Database Log log.Logger StateStore *AutosavingStateStore - users map[types.MatrixUserID]*User + users map[types.MatrixUserID]*User + managementRooms map[types.MatrixRoomID]*User } func NewBridge() *Bridge { bridge := &Bridge{ - users: make(map[types.MatrixUserID]*User), + users: make(map[types.MatrixUserID]*User), + managementRooms: make(map[types.MatrixRoomID]*User), } var err error bridge.Config, err = config.Load(*configPath) @@ -111,23 +113,28 @@ func (bridge *Bridge) Init() { os.Exit(13) } - bridge.Log.Debugln("Initializing event processor") + bridge.Log.Debugln("Initializing Matrix event processor") bridge.EventProcessor = appservice.NewEventProcessor(bridge.AppService) - bridge.EventProcessor.On(gomatrix.EventMessage, bridge.HandleMessage) - bridge.EventProcessor.On(gomatrix.StateMember, bridge.HandleMembership) + bridge.Log.Debugln("Initializing Matrix event handler") + bridge.MatrixHandler = NewMatrixHandler(bridge) } func (bridge *Bridge) Start() { - bridge.DB.CreateTables() + err := bridge.DB.CreateTables() + if err != nil { + bridge.Log.Fatalln("Failed to create database tables:", err) + os.Exit(14) + } bridge.Log.Debugln("Starting application service HTTP server") go bridge.AppService.Start() bridge.Log.Debugln("Starting event processor") go bridge.EventProcessor.Start() - bridge.Log.Debugln("Updating bot profile") go bridge.UpdateBotProfile() + go bridge.StartUsers() } func (bridge *Bridge) UpdateBotProfile() { + bridge.Log.Debugln("Updating bot profile") botConfig := bridge.Config.AppService.Bot var err error @@ -150,6 +157,12 @@ func (bridge *Bridge) UpdateBotProfile() { } } +func (bridge *Bridge) StartUsers() { + for _, user := range bridge.GetAllUsers() { + go user.Start() + } +} + func (bridge *Bridge) Stop() { bridge.AppService.Stop() bridge.EventProcessor.Stop() diff --git a/matrix.go b/matrix.go index f69ff9b..e21ceda 100644 --- a/matrix.go +++ b/matrix.go @@ -18,26 +18,49 @@ package main import ( "maunium.net/go/gomatrix" + "maunium.net/go/mautrix-whatsapp/types" + "maunium.net/go/mautrix-appservice" + "maunium.net/go/maulogger" + "strings" ) -func (bridge *Bridge) HandleBotInvite(evt *gomatrix.Event) { - intent := bridge.AppService.BotIntent() +type MatrixHandler struct { + bridge *Bridge + as *appservice.AppService + log maulogger.Logger + cmd *CommandHandler +} + +func NewMatrixHandler(bridge *Bridge) *MatrixHandler { + handler := &MatrixHandler{ + bridge: bridge, + as: bridge.AppService, + log: bridge.Log.Sub("Matrix"), + cmd: NewCommandHandler(bridge), + } + bridge.EventProcessor.On(gomatrix.EventMessage, handler.HandleMessage) + bridge.EventProcessor.On(gomatrix.StateMember, handler.HandleMembership) + return handler +} + +func (mx *MatrixHandler) HandleBotInvite(evt *gomatrix.Event) { + intent := mx.as.BotIntent() resp, err := intent.JoinRoom(evt.RoomID, "", nil) if err != nil { - bridge.Log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender) + mx.log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender) return } members, err := intent.JoinedMembers(resp.RoomID) if err != nil { - bridge.Log.Debugln("Failed to get members in room", resp.RoomID, "after accepting invite from", evt.Sender) + mx.log.Debugln("Failed to get members in room", resp.RoomID, "after accepting invite from", evt.Sender) intent.LeaveRoom(resp.RoomID) return } if len(members.Joined) < 2 { - bridge.Log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender) + mx.log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender) intent.LeaveRoom(resp.RoomID) return } @@ -46,31 +69,49 @@ func (bridge *Bridge) HandleBotInvite(evt *gomatrix.Event) { for mxid, _ := range members.Joined { if mxid == intent.UserID || mxid == evt.Sender { continue - } else if _, _, ok := bridge.ParsePuppetMXID(mxid); ok { + } else if _, _, ok := mx.bridge.ParsePuppetMXID(types.MatrixUserID(mxid)); ok { hasPuppets = true continue } - bridge.Log.Debugln("Leaving multi-user room", resp.RoomID, "after accepting invite from", evt.Sender) + mx.log.Debugln("Leaving multi-user room", resp.RoomID, "after accepting invite from", evt.Sender) intent.SendNotice(resp.RoomID, "This bridge is user-specific, please don't invite me into rooms with other users.") intent.LeaveRoom(resp.RoomID) return } if !hasPuppets { - user := bridge.GetUser(evt.Sender) - user.ManagementRoom = resp.RoomID - user.Update() - intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room.") - bridge.Log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender) + user := mx.bridge.GetUser(types.MatrixUserID(evt.Sender)) + user.SetManagementRoom(types.MatrixRoomID(resp.RoomID)) + intent.SendNotice(string(user.ManagementRoom), "This room has been registered as your bridge management/status room.") + mx.log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender) } } -func (bridge *Bridge) HandleMembership(evt *gomatrix.Event) { - bridge.Log.Debugln(evt.Content, evt.Content.Membership, evt.GetStateKey()) - if evt.Content.Membership == "invite" && evt.GetStateKey() == bridge.AppService.BotMXID() { - bridge.HandleBotInvite(evt) +func (mx *MatrixHandler) HandleMembership(evt *gomatrix.Event) { + mx.log.Debugln(evt.Content, evt.Content.Membership, evt.GetStateKey()) + if evt.Content.Membership == "invite" && evt.GetStateKey() == mx.as.BotMXID() { + mx.HandleBotInvite(evt) } } -func (bridge *Bridge) HandleMessage(evt *gomatrix.Event) { +func (mx *MatrixHandler) HandleMessage(evt *gomatrix.Event) { + roomID := types.MatrixRoomID(evt.RoomID) + user := mx.bridge.GetUser(types.MatrixUserID(evt.Sender)) + + if evt.Content.MsgType == gomatrix.MsgText { + commandPrefix := mx.bridge.Config.Bridge.CommandPrefix + hasCommandPrefix := strings.HasPrefix(evt.Content.Body, commandPrefix) + if hasCommandPrefix { + evt.Content.Body = strings.TrimLeft(evt.Content.Body[len(commandPrefix):], " ") + } + if hasCommandPrefix || roomID == user.ManagementRoom { + mx.cmd.Handle(roomID, user, evt.Content.Body) + return + } + } + + portal := user.GetPortalByMXID(roomID) + if portal != nil { + portal.HandleMessage(evt) + } } diff --git a/portal.go b/portal.go index 4d35185..2dfc5e0 100644 --- a/portal.go +++ b/portal.go @@ -21,13 +21,16 @@ import ( log "maunium.net/go/maulogger" "fmt" "maunium.net/go/mautrix-whatsapp/types" + "maunium.net/go/gomatrix" + "strings" + "maunium.net/go/mautrix-appservice" ) func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal { portal, ok := user.portalsByMXID[mxid] if !ok { dbPortal := user.bridge.DB.Portal.GetByMXID(mxid) - if dbPortal == nil || dbPortal.Owner != user.UserID { + if dbPortal == nil || dbPortal.Owner != user.ID { return nil } portal = user.NewPortal(dbPortal) @@ -42,9 +45,12 @@ func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal { func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal { portal, ok := user.portalsByJID[jid] if !ok { - dbPortal := user.bridge.DB.Portal.GetByJID(user.UserID, jid) + dbPortal := user.bridge.DB.Portal.GetByJID(user.ID, jid) if dbPortal == nil { - return nil + dbPortal = user.bridge.DB.Portal.New() + dbPortal.JID = jid + dbPortal.Owner = user.ID + dbPortal.Insert() } portal = user.NewPortal(dbPortal) user.portalsByJID[portal.JID] = portal @@ -56,7 +62,7 @@ func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal { } func (user *User) GetAllPortals() []*Portal { - dbPortals := user.bridge.DB.Portal.GetAll(user.UserID) + dbPortals := user.bridge.DB.Portal.GetAll(user.ID) output := make([]*Portal, len(dbPortals)) for index, dbPortal := range dbPortals { portal, ok := user.portalsByJID[dbPortal.JID] @@ -88,3 +94,48 @@ type Portal struct { bridge *Bridge log log.Logger } + +func (portal *Portal) CreateMatrixRoom() error { + if len(portal.MXID) > 0 { + return nil + } + + name := portal.Name + topic := "" + isPrivateChat := false + if strings.HasSuffix(portal.JID, "s.whatsapp.net") { + puppet := portal.user.GetPuppetByJID(portal.JID) + name = puppet.Displayname + topic = "WhatsApp private chat" + isPrivateChat = true + } + resp, err := portal.MainIntent().CreateRoom(&gomatrix.ReqCreateRoom{ + Visibility: "private", + Name: name, + Topic: topic, + Invite: []string{portal.user.ID}, + Preset: "private_chat", + IsDirect: isPrivateChat, + }) + if err != nil { + return err + } + portal.MXID = resp.RoomID + portal.Update() + return nil +} + +func (portal *Portal) IsPrivateChat() bool { + return strings.HasSuffix(portal.JID, puppetJIDStrippedSuffix) +} + +func (portal *Portal) MainIntent() *appservice.IntentAPI { + if portal.IsPrivateChat() { + return portal.user.GetPuppetByJID(portal.JID).Intent() + } + return portal.bridge.AppService.BotIntent() +} + +func (portal *Portal) HandleMessage(evt *gomatrix.Event) { + portal.log.Debugln("Received event:", evt) +} diff --git a/puppet.go b/puppet.go index 417f564..0d15975 100644 --- a/puppet.go +++ b/puppet.go @@ -23,8 +23,11 @@ import ( "regexp" "maunium.net/go/mautrix-whatsapp/types" "strings" + "maunium.net/go/mautrix-appservice" ) +const puppetJIDStrippedSuffix = "@s.whatsapp.net" + func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUserID, types.WhatsAppID, bool) { userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", bridge.Config.Bridge.FormatUsername("([0-9]+)", "([0-9]+)"), @@ -38,11 +41,12 @@ func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUser return "", "", false } - receiver := match[1] + receiver := types.MatrixUserID(match[1]) receiver = strings.Replace(receiver, "=40", "@", 1) colonIndex := strings.LastIndex(receiver, "=3") receiver = receiver[:colonIndex] + ":" + receiver[colonIndex+len("=3"):] - return types.MatrixUserID(receiver), types.WhatsAppID(match[2]), true + jid := types.WhatsAppID(match[2] + puppetJIDStrippedSuffix) + return receiver, jid, true } func (bridge *Bridge) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet { @@ -61,7 +65,7 @@ func (bridge *Bridge) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet { func (user *User) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet { receiver, jid, ok := user.bridge.ParsePuppetMXID(mxid) - if !ok || receiver != user.UserID { + if !ok || receiver != user.ID { return nil } @@ -71,9 +75,12 @@ func (user *User) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet { func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet { puppet, ok := user.puppets[jid] if !ok { - dbPuppet := user.bridge.DB.Puppet.Get(jid, user.UserID) + dbPuppet := user.bridge.DB.Puppet.Get(jid, user.ID) if dbPuppet == nil { - return nil + dbPuppet = user.bridge.DB.Puppet.New() + dbPuppet.JID = jid + dbPuppet.Receiver = user.ID + dbPuppet.Insert() } puppet = user.NewPuppet(dbPuppet) user.puppets[puppet.JID] = puppet @@ -82,7 +89,7 @@ func (user *User) GetPuppetByJID(jid types.WhatsAppID) *Puppet { } func (user *User) GetAllPuppets() []*Puppet { - dbPuppets := user.bridge.DB.Puppet.GetAll(user.UserID) + dbPuppets := user.bridge.DB.Puppet.GetAll(user.ID) output := make([]*Puppet, len(dbPuppets)) for index, dbPuppet := range dbPuppets { puppet, ok := user.puppets[dbPuppet.JID] @@ -101,6 +108,14 @@ func (user *User) NewPuppet(dbPuppet *database.Puppet) *Puppet { user: user, bridge: user.bridge, log: user.log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)), + + MXID: fmt.Sprintf("@%s:%s", + user.bridge.Config.Bridge.FormatUsername( + dbPuppet.Receiver, + strings.Replace( + dbPuppet.JID, + puppetJIDStrippedSuffix, "", 1)), + user.bridge.Config.Homeserver.Domain), } } @@ -110,4 +125,10 @@ type Puppet struct { user *User bridge *Bridge log log.Logger + + MXID types.MatrixUserID +} + +func (puppet *Puppet) Intent() *appservice.IntentAPI { + return puppet.bridge.AppService.Intent(puppet.MXID) } diff --git a/user.go b/user.go index 6a98275..f34e5d8 100644 --- a/user.go +++ b/user.go @@ -20,11 +20,11 @@ import ( "maunium.net/go/mautrix-whatsapp/database" "github.com/Rhymen/go-whatsapp" "time" - "fmt" - "os" "github.com/skip2/go-qrcode" log "maunium.net/go/maulogger" "maunium.net/go/mautrix-whatsapp/types" + "strings" + "encoding/json" ) type User struct { @@ -45,10 +45,14 @@ func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User { dbUser := bridge.DB.User.Get(userID) if dbUser == nil { dbUser = bridge.DB.User.New() + dbUser.ID = userID dbUser.Insert() } user = bridge.NewUser(dbUser) - bridge.users[user.UserID] = user + bridge.users[user.ID] = user + if len(user.ManagementRoom) > 0 { + bridge.managementRooms[user.ManagementRoom] = user + } } return user } @@ -57,57 +61,87 @@ func (bridge *Bridge) GetAllUsers() []*User { dbUsers := bridge.DB.User.GetAll() output := make([]*User, len(dbUsers)) for index, dbUser := range dbUsers { - user, ok := bridge.users[dbUser.UserID] + user, ok := bridge.users[dbUser.ID] if !ok { user = bridge.NewUser(dbUser) - bridge.users[user.UserID] = user + bridge.users[user.ID] = user + if len(user.ManagementRoom) > 0 { + bridge.managementRooms[user.ManagementRoom] = user + } } output[index] = user } return output } -func (bridge *Bridge) InitWhatsApp() { - users := bridge.GetAllUsers() - for _, user := range users { - user.Connect() - } -} - func (bridge *Bridge) NewUser(dbUser *database.User) *User { return &User{ - User: dbUser, - bridge: bridge, - log: bridge.Log.Sub("User").Sub(dbUser.UserID), + User: dbUser, + bridge: bridge, + log: bridge.Log.Sub("User").Sub(string(dbUser.ID)), + portalsByMXID: make(map[types.MatrixRoomID]*Portal), + portalsByJID: make(map[types.WhatsAppID]*Portal), + puppets: make(map[types.WhatsAppID]*Puppet), } } -func (user *User) Connect() { +func (user *User) SetManagementRoom(roomID types.MatrixRoomID) { + existingUser, ok := user.bridge.managementRooms[roomID] + if ok { + existingUser.ManagementRoom = "" + existingUser.Update() + } + + user.ManagementRoom = roomID + user.bridge.managementRooms[user.ManagementRoom] = user + user.Update() +} + +func (user *User) SetSession(session *whatsapp.Session) { + user.Session = session + user.Update() +} + +func (user *User) Start() { + if user.Connect(false) { + user.Sync() + } +} + +func (user *User) Connect(evenIfNoSession bool) bool { + if user.Conn != nil { + return true + } else if !evenIfNoSession && user.Session == nil { + return false + } + user.log.Debugln("Connecting to WhatsApp") var err error user.Conn, err = whatsapp.NewConn(20 * time.Second) if err != nil { user.log.Errorln("Failed to connect to WhatsApp:", err) - return + return false } + user.log.Debugln("WhatsApp connection successful") user.Conn.AddHandler(user) - user.RestoreSession() + return user.RestoreSession() } -func (user *User) RestoreSession() { +func (user *User) RestoreSession() bool { if user.Session != nil { sess, err := user.Conn.RestoreSession(*user.Session) if err != nil { user.log.Errorln("Failed to restore session:", err) - user.Session = nil - return + //user.SetSession(nil) + return false } - user.Session = &sess - user.log.Debugln("Session restored") + user.SetSession(&sess) + user.log.Debugln("Session restored successfully") + return true } - return + return false } -func (user *User) Login(roomID string) { +func (user *User) Login(roomID types.MatrixRoomID) { bot := user.bridge.AppService.BotClient() qrChan := make(chan string, 2) @@ -130,7 +164,7 @@ func (user *User) Login(roomID string) { return } - bot.SendImage(roomID, string(qrCode), resp.ContentURI) + bot.SendImage(roomID, string(code), resp.ContentURI) }() session, err := user.Conn.Login(qrChan) if err != nil { @@ -145,32 +179,79 @@ func (user *User) Login(roomID string) { go user.Sync() } -func (user *User) Sync() { - chats, err := user.Conn.Chats() - if err != nil { - user.log.Warnln("Failed to get chats") - return +func (user *User) SyncPuppet(contact whatsapp.Contact) { + puppet := user.GetPuppetByJID(contact.Jid) + puppet.Intent().EnsureRegistered() + + newName := user.bridge.Config.Bridge.FormatDisplayname(contact) + puppet.log.Debugln(puppet.Displayname, newName, contact.Name) + if puppet.Displayname != newName { + puppet.Displayname = newName + puppet.Update() + puppet.Intent().SetDisplayName(puppet.Displayname) + } +} + +func (user *User) SyncPortal(contact whatsapp.Contact) { + portal := user.GetPortalByJID(contact.Jid) + + if len(portal.MXID) == 0 { + if !portal.IsPrivateChat() { + portal.Name = contact.Name + } + err := portal.CreateMatrixRoom() + if err != nil { + user.log.Errorln("Failed to create portal:", err) + return + } + } + + if !portal.IsPrivateChat() && portal.Name != contact.Name { + portal.Name = contact.Name + portal.Update() + // TODO add SetRoomName function to intent API + portal.MainIntent().SendStateEvent(portal.MXID, "m.room.name", "", map[string]interface{}{ + "name": portal.Name, + }) + } +} + +func (user *User) Sync() { + user.log.Debugln("Syncing...") + user.Conn.Contacts() + user.log.Debugln(user.Conn.Store.Contacts) + for jid, contact := range user.Conn.Store.Contacts { + dat, _ := json.Marshal(&contact) + user.log.Debugln(string(dat)) + if strings.HasSuffix(jid, puppetJIDStrippedSuffix) { + user.SyncPuppet(contact) + } + + if len(contact.Notify) == 0 && !strings.HasSuffix(jid, "@g.us") { + // Don't bridge yet + continue + } + + user.SyncPortal(contact) } - user.log.Debugln(chats) } func (user *User) HandleError(err error) { user.log.Errorln("WhatsApp error:", err) - fmt.Fprintf(os.Stderr, "%v", err) } func (user *User) HandleTextMessage(message whatsapp.TextMessage) { - fmt.Println(message) + user.log.Debugln("Text message:", message) } func (user *User) HandleImageMessage(message whatsapp.ImageMessage) { - fmt.Println(message) + user.log.Debugln("Image message:", message) } func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) { - fmt.Println(message) + user.log.Debugln("Video message:", message) } func (user *User) HandleJsonMessage(message string) { - fmt.Println(message) + user.log.Debugln("JSON message:", message) }