diff --git a/ROADMAP.md b/ROADMAP.md index 2971d1e..3d53bb5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -59,7 +59,7 @@ * [ ] When receiving invite[2] * [x] When receiving message * [ ] Private chat creation by inviting Matrix puppet of WhatsApp user to new room - * [ ] Option to use own Matrix account for messages sent from WhatsApp mobile/other web clients + * [x] Option to use own Matrix account for messages sent from WhatsApp mobile/other web clients * [x] Shared group chat portals [1] May involve reverse-engineering the WhatsApp Web API and/or editing go-whatsapp diff --git a/commands.go b/commands.go index 1fff5c0..ad74dc7 100644 --- a/commands.go +++ b/commands.go @@ -18,10 +18,12 @@ package main import ( "fmt" - "github.com/Rhymen/go-whatsapp" + "strings" + "maunium.net/go/mautrix" "maunium.net/go/mautrix/format" - "strings" + + "github.com/Rhymen/go-whatsapp" "maunium.net/go/maulogger/v2" "maunium.net/go/mautrix-appservice" @@ -80,6 +82,8 @@ func (handler *CommandHandler) Handle(roomID types.MatrixRoomID, user *User, mes switch cmd { case "login": handler.CommandLogin(ce) + case "logout-matrix": + handler.CommandLogoutMatrix(ce) case "help": handler.CommandHelp(ce) case "reconnect": @@ -92,7 +96,7 @@ func (handler *CommandHandler) Handle(roomID types.MatrixRoomID, user *User, mes handler.CommandDeleteSession(ce) case "delete-portal": handler.CommandDeletePortal(ce) - case "logout", "sync", "list", "open", "pm": + case "login-matrix", "logout", "sync", "list", "open", "pm": if ce.User.Conn == nil { ce.Reply("You are not logged in. Use the `login` command to log into WhatsApp.") return @@ -102,6 +106,8 @@ func (handler *CommandHandler) Handle(roomID types.MatrixRoomID, user *User, mes } switch cmd { + case "login-matrix": + handler.CommandLoginMatrix(ce) case "logout": handler.CommandLogout(ce) case "sync": @@ -433,3 +439,25 @@ func (handler *CommandHandler) CommandPM(ce *CommandEvent) { } ce.Reply("Created portal room and invited you to it.") } + +const cmdLoginMatrixHelp = `login-matrix <_access token_> - Replace your WhatsApp account's Matrix puppet with your real Matrix account.'` + +func (handler *CommandHandler) CommandLoginMatrix(ce *CommandEvent) { + if len(ce.Args) == 0 { + ce.Reply("**Usage:** `login-matrix `") + return + } + puppet := handler.bridge.GetPuppetByJID(ce.User.JID) + err := puppet.SwitchCustomMXID(ce.Args[0], ce.User.MXID) + if err != nil { + ce.Reply("Failed to switch puppet: %v", err) + return + } + ce.Reply("Successfully switched puppet") +} + +const cmdLogoutMatrixHelp = `logout-matrix - Switch your WhatsApp account's Matrix puppet back to the default one.` + +func (handler *CommandHandler) CommandLogoutMatrix(ce *CommandEvent) { + +} diff --git a/config/bridge.go b/config/bridge.go index ee3381f..b4a800a 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -43,6 +43,8 @@ type BridgeConfig struct { RecoverHistory bool `yaml:"recovery_history_backfill"` SyncChatMaxAge uint64 `yaml:"sync_max_chat_age"` + SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"` + CommandPrefix string `yaml:"command_prefix"` Permissions PermissionConfig `yaml:"permissions"` @@ -61,6 +63,8 @@ func (bc *BridgeConfig) setDefaults() { bc.RecoverChatSync = -1 bc.RecoverHistory = true bc.SyncChatMaxAge = 259200 + + bc.SyncWithCustomPuppets = true } type umBridgeConfig BridgeConfig diff --git a/custompuppet.go b/custompuppet.go new file mode 100644 index 0000000..33b0c3d --- /dev/null +++ b/custompuppet.go @@ -0,0 +1,168 @@ +// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 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 ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/pkg/errors" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix-appservice" +) + +var ( + ErrNoCustomMXID = errors.New("no custom mxid set") + ErrMismatchingMXID = errors.New("whoami result does not match custom mxid") +) + +func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid string) error { + prevCustomMXID := puppet.CustomMXID + if puppet.customIntent != nil { + puppet.stopSyncing() + } + puppet.CustomMXID = mxid + puppet.AccessToken = accessToken + + err := puppet.StartCustomMXID() + if err != nil { + return err + } + + if len(prevCustomMXID) > 0 { + delete(puppet.bridge.puppetsByCustomMXID, prevCustomMXID) + } + if len(puppet.CustomMXID) > 0 { + puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet + } + puppet.Update() + // TODO leave rooms with default puppet + return nil +} + +func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) { + if len(puppet.CustomMXID) == 0 { + return nil, ErrNoCustomMXID + } + client, err := mautrix.NewClient(puppet.bridge.AS.HomeserverURL, puppet.CustomMXID, puppet.AccessToken) + if err != nil { + return nil, err + } + client.Store = puppet + + ia := puppet.bridge.AS.NewIntentAPI("custom") + ia.Client = client + ia.Localpart = puppet.CustomMXID[1:strings.IndexRune(puppet.CustomMXID, ':')] + ia.UserID = puppet.CustomMXID + ia.IsCustomPuppet = true + return ia, nil +} + +func (puppet *Puppet) StartCustomMXID() error { + if len(puppet.CustomMXID) == 0 { + return nil + } + intent, err := puppet.newCustomIntent() + if err != nil { + puppet.CustomMXID = "" + puppet.AccessToken = "" + return err + } + urlPath := intent.BuildURL("account", "whoami") + var resp struct{ UserID string `json:"user_id"` } + _, err = intent.MakeRequest("GET", urlPath, nil, &resp) + if err != nil { + puppet.CustomMXID = "" + puppet.AccessToken = "" + return err + } + if resp.UserID != puppet.CustomMXID { + puppet.CustomMXID = "" + puppet.AccessToken = "" + return ErrMismatchingMXID + } + puppet.customIntent = intent + puppet.startSyncing() + return nil +} + +func (puppet *Puppet) startSyncing() { + if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets { + return + } + go func() { + puppet.log.Debugln("Starting syncing...") + err := puppet.customIntent.Sync() + if err != nil { + puppet.log.Errorln("Fatal error syncing:", err) + } + }() +} + +func (puppet *Puppet) stopSyncing() { + if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets { + return + } + puppet.customIntent.StopSync() +} + +func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, since string) error { + puppet.log.Debugln("Sync data:", resp, since) + // TODO handle sync data + return nil +} + +func (puppet *Puppet) OnFailedSync(res *mautrix.RespSync, err error) (time.Duration, error) { + puppet.log.Warnln("Sync error:", err) + return 10 * time.Second, nil +} + +func (puppet *Puppet) GetFilterJSON(_ string) json.RawMessage { + mxid, _ := json.Marshal(puppet.CustomMXID) + return json.RawMessage(fmt.Sprintf(`{ + "account_data": { "types": [] }, + "presence": { + "senders": [ + %s + ], + "types": [ + "m.presence" + ] + }, + "room": { + "ephemeral": { + "types": [ + "m.typing", + "m.receipt" + ] + }, + "include_leave": false, + "account_data": { "types": [] }, + "state": { "types": [] }, + "timeline": { "types": [] } + } +}`, mxid)) +} + +func (puppet *Puppet) SaveFilterID(_, _ string) {} +func (puppet *Puppet) SaveNextBatch(_, nbt string) { puppet.NextBatch = nbt } +func (puppet *Puppet) SaveRoom(room *mautrix.Room) {} +func (puppet *Puppet) LoadFilterID(_ string) string { return "" } +func (puppet *Puppet) LoadNextBatch(_ string) string { return puppet.NextBatch } +func (puppet *Puppet) LoadRoom(roomID string) *mautrix.Room { return nil } diff --git a/database/puppet.go b/database/puppet.go index c411420..66fc5ee 100644 --- a/database/puppet.go +++ b/database/puppet.go @@ -56,6 +56,26 @@ func (pq *PuppetQuery) Get(jid types.WhatsAppID) *Puppet { return pq.New().Scan(row) } +func (pq *PuppetQuery) GetByCustomMXID(mxid types.MatrixUserID) *Puppet { + row := pq.db.QueryRow("SELECT * FROM puppet WHERE custom_mxid=$1", mxid) + if row == nil { + return nil + } + return pq.New().Scan(row) +} + +func (pq *PuppetQuery) GetAllWithCustomMXID() (puppets []*Puppet) { + rows, err := pq.db.Query("SELECT * FROM puppet WHERE custom_mxid<>''") + if err != nil || rows == nil { + return nil + } + defer rows.Close() + for rows.Next() { + puppets = append(puppets, pq.New().Scan(rows)) + } + return +} + type Puppet struct { db *Database log log.Logger @@ -64,12 +84,16 @@ type Puppet struct { Avatar string Displayname string NameQuality int8 + + CustomMXID string + AccessToken string + NextBatch string } func (puppet *Puppet) Scan(row Scannable) *Puppet { - var displayname, avatar sql.NullString + var displayname, avatar, customMXID, accessToken, nextBatch sql.NullString var quality sql.NullInt64 - err := row.Scan(&puppet.JID, &avatar, &displayname, &quality) + err := row.Scan(&puppet.JID, &avatar, &displayname, &quality, &customMXID, &accessToken, &nextBatch) if err != nil { if err != sql.ErrNoRows { puppet.log.Errorln("Database scan failed:", err) @@ -79,20 +103,23 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet { puppet.Displayname = displayname.String puppet.Avatar = avatar.String puppet.NameQuality = int8(quality.Int64) + puppet.CustomMXID = customMXID.String + puppet.AccessToken = accessToken.String + puppet.NextBatch = nextBatch.String return puppet } func (puppet *Puppet) Insert() { - _, err := puppet.db.Exec("INSERT INTO puppet VALUES ($1, $2, $3, $4)", - puppet.JID, puppet.Avatar, puppet.Displayname, puppet.NameQuality) + _, err := puppet.db.Exec("INSERT INTO puppet VALUES ($1, $2, $3, $4, $5, $6, $7)", + puppet.JID, puppet.Avatar, puppet.Displayname, puppet.NameQuality, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch) if err != nil { puppet.log.Warnfln("Failed to insert %s: %v", puppet.JID, err) } } func (puppet *Puppet) Update() { - _, err := puppet.db.Exec("UPDATE puppet SET displayname=$1, name_quality=$2, avatar=$3 WHERE jid=$4", - puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.JID) + _, err := puppet.db.Exec("UPDATE puppet SET displayname=$1, name_quality=$2, avatar=$3, custom_mxid=$4, access_token=$5, next_batch=$6 WHERE jid=$7", + puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.JID) if err != nil { puppet.log.Warnfln("Failed to update %s->%s: %v", puppet.JID, err) } diff --git a/database/upgrades/2019-05-23-puppet-custom-mxid-columns.go b/database/upgrades/2019-05-23-puppet-custom-mxid-columns.go new file mode 100644 index 0000000..c99d521 --- /dev/null +++ b/database/upgrades/2019-05-23-puppet-custom-mxid-columns.go @@ -0,0 +1,23 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[5] = upgrade{"Add columns to store custom puppet info", func(dialect Dialect, tx *sql.Tx, db *sql.DB) error { + _, err := tx.Exec(`ALTER TABLE puppet ADD COLUMN custom_mxid VARCHAR(255)`) + if err != nil { + return err + } + _, err = tx.Exec(`ALTER TABLE puppet ADD COLUMN access_token VARCHAR(1023)`) + if err != nil { + return err + } + _, err = tx.Exec(`ALTER TABLE puppet ADD COLUMN next_batch VARCHAR(255)`) + if err != nil { + return err + } + return nil + }} +} diff --git a/database/upgrades/upgrades.go b/database/upgrades/upgrades.go index 5e98872..5366198 100644 --- a/database/upgrades/upgrades.go +++ b/database/upgrades/upgrades.go @@ -22,7 +22,9 @@ type upgrade struct { fn upgradeFunc } -var upgrades [5]upgrade +const NumberOfUpgrades = 6 + +var upgrades [NumberOfUpgrades]upgrade func getVersion(dialect Dialect, db *sql.DB) (int, error) { _, err := db.Exec("CREATE TABLE IF NOT EXISTS version (version INTEGER)") @@ -63,7 +65,7 @@ func Run(log log.Logger, dialectName string, db *sql.DB) error { return err } - log.Infofln("Database currently on v%d, latest: v%d", version, len(upgrades)) + log.Infofln("Database currently on v%d, latest: v%d", version, NumberOfUpgrades) for i, upgrade := range upgrades[version:] { log.Infofln("Upgrading database to v%d: %s", version+i+1, upgrade.message) tx, err := db.Begin() diff --git a/example-config.yaml b/example-config.yaml index 4970bee..c5546a9 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -81,6 +81,10 @@ bridge: # Default is 3 days = 259200 seconds sync_max_chat_age: 259200 + # Whether or not to sync with custom puppets to receive EDUs that + # are not normally sent to appservices. + sync_with_custom_puppets: true + # The prefix for commands. Only required in non-management rooms. command_prefix: "!wa" diff --git a/go.mod b/go.mod index 3dd07c3..1e2d1a9 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/lib/pq v1.1.1 github.com/mattn/go-isatty v0.0.8 // indirect github.com/mattn/go-sqlite3 v1.10.0 + github.com/pkg/errors v0.8.1 github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect @@ -17,9 +18,10 @@ require ( maunium.net/go/mauflag v1.0.0 maunium.net/go/maulogger/v2 v2.0.0 maunium.net/go/mautrix v0.1.0-alpha.3.0.20190515215109-3e27638f3f1d - maunium.net/go/mautrix-appservice v0.1.0-alpha.3.0.20190515184712-aecd1f0cca6f + maunium.net/go/mautrix-appservice v0.1.0-alpha.3.0.20190523231710-8b9923f4ca89 ) replace gopkg.in/russross/blackfriday.v2 => github.com/russross/blackfriday/v2 v2.0.1 -replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.0.2-0.20190523194501-cc7603f853df +//replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.0.2-0.20190523194501-cc7603f853df +replace github.com/Rhymen/go-whatsapp => ../../Go/go-whatsapp diff --git a/main.go b/main.go index 0a42ac4..666a721 100644 --- a/main.go +++ b/main.go @@ -80,17 +80,19 @@ type Bridge struct { portalsByJID map[database.PortalKey]*Portal portalsLock sync.Mutex puppets map[types.WhatsAppID]*Puppet + puppetsByCustomMXID map[types.MatrixUserID]*Puppet puppetsLock sync.Mutex } func NewBridge() *Bridge { bridge := &Bridge{ - usersByMXID: make(map[types.MatrixUserID]*User), - usersByJID: make(map[types.WhatsAppID]*User), - managementRooms: make(map[types.MatrixRoomID]*User), - portalsByMXID: make(map[types.MatrixRoomID]*Portal), - portalsByJID: make(map[database.PortalKey]*Portal), - puppets: make(map[types.WhatsAppID]*Puppet), + usersByMXID: make(map[types.MatrixUserID]*User), + usersByJID: make(map[types.WhatsAppID]*User), + managementRooms: make(map[types.MatrixRoomID]*User), + portalsByMXID: make(map[types.MatrixRoomID]*Portal), + portalsByJID: make(map[database.PortalKey]*Portal), + puppets: make(map[types.WhatsAppID]*Puppet), + puppetsByCustomMXID: make(map[types.MatrixUserID]*Puppet), } var err error @@ -192,6 +194,16 @@ func (bridge *Bridge) StartUsers() { for _, user := range bridge.GetAllUsers() { go user.Connect(false) } + bridge.Log.Debugln("Starting custom puppets") + for _, puppet := range bridge.GetAllPuppetsWithCustomMXID() { + go func() { + puppet.log.Debugln("Starting custom puppet", puppet.CustomMXID) + err := puppet.StartCustomMXID() + if err != nil { + puppet.log.Errorln("Failed to start custom puppet:", err) + } + }() + } } func (bridge *Bridge) Stop() { diff --git a/matrix.go b/matrix.go index c2c5c8d..69e0e75 100644 --- a/matrix.go +++ b/matrix.go @@ -166,6 +166,10 @@ func (mx *MatrixHandler) HandleMessage(evt *mautrix.Event) { if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet { return } + isCustomPuppet, ok := evt.Content.Raw["net.maunium.whatsapp.puppet"].(bool) + if ok && isCustomPuppet && mx.bridge.GetPuppetByCustomMXID(evt.Sender) != nil { + return + } roomID := types.MatrixRoomID(evt.RoomID) user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender)) diff --git a/portal.go b/portal.go index ac7427b..69828be 100644 --- a/portal.go +++ b/portal.go @@ -281,12 +281,6 @@ func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) { changed = true } for _, participant := range metadata.Participants { - puppet := portal.bridge.GetPuppetByJID(participant.JID) - err := puppet.Intent().EnsureJoined(portal.MXID) - if err != nil { - portal.log.Warnfln("Failed to make puppet of %s join %s: %v", participant.JID, portal.MXID, err) - } - user := portal.bridge.GetUserByJID(participant.JID) if user != nil && !portal.bridge.AS.StateStore.IsInvited(portal.MXID, user.MXID) { _, err = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{ @@ -297,6 +291,12 @@ func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) { } } + puppet := portal.bridge.GetPuppetByJID(participant.JID) + err := puppet.IntentFor(portal).EnsureJoined(portal.MXID) + if err != nil { + portal.log.Warnfln("Failed to make puppet of %s join %s: %v", participant.JID, portal.MXID, err) + } + expectedLevel := 0 if participant.IsSuperAdmin { expectedLevel = 95 @@ -363,7 +363,7 @@ func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID) bool { if portal.Name != name { intent := portal.MainIntent() if len(setBy) > 0 { - intent = portal.bridge.GetPuppetByJID(setBy).Intent() + intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) } _, err := intent.SetRoomName(portal.MXID, name) if err == nil { @@ -379,7 +379,7 @@ func (portal *Portal) UpdateTopic(topic string, setBy types.WhatsAppID) bool { if portal.Topic != topic { intent := portal.MainIntent() if len(setBy) > 0 { - intent = portal.bridge.GetPuppetByJID(setBy).Intent() + intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) } _, err := intent.SetRoomTopic(portal.MXID, topic) if err == nil { @@ -719,7 +719,7 @@ func (portal *Portal) IsStatusBroadcastRoom() bool { func (portal *Portal) MainIntent() *appservice.IntentAPI { if portal.IsPrivateChat() { - return portal.bridge.GetPuppetByJID(portal.Key.JID).Intent() + return portal.bridge.GetPuppetByJID(portal.Key.JID).DefaultIntent() } return portal.bridge.Bot } @@ -727,10 +727,9 @@ func (portal *Portal) MainIntent() *appservice.IntentAPI { func (portal *Portal) GetMessageIntent(user *User, info whatsapp.MessageInfo) *appservice.IntentAPI { if info.FromMe { if portal.IsPrivateChat() { - // TODO handle own messages in private chats properly - return nil + return portal.bridge.GetPuppetByJID(user.JID).CustomIntent() } - return portal.bridge.GetPuppetByJID(user.JID).Intent() + return portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal) } else if portal.IsPrivateChat() { return portal.MainIntent() } else if len(info.SenderJid) == 0 { @@ -740,7 +739,7 @@ func (portal *Portal) GetMessageIntent(user *User, info whatsapp.MessageInfo) *a return nil } } - return portal.bridge.GetPuppetByJID(info.SenderJid).Intent() + return portal.bridge.GetPuppetByJID(info.SenderJid).IntentFor(portal) } func (portal *Portal) SetReply(content *mautrix.Content, info whatsapp.MessageInfo) { @@ -765,15 +764,18 @@ func (portal *Portal) HandleMessageRevoke(user *User, message whatsappExt.Messag if msg == nil { return } - intent := portal.MainIntent() + var intent *appservice.IntentAPI if message.FromMe { if portal.IsPrivateChat() { - // TODO handle + intent = portal.bridge.GetPuppetByJID(user.JID).CustomIntent() } else { - intent = portal.bridge.GetPuppetByJID(user.JID).Intent() + intent = portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal) } } else if len(message.Participant) > 0 { - intent = portal.bridge.GetPuppetByJID(message.Participant).Intent() + intent = portal.bridge.GetPuppetByJID(message.Participant).IntentFor(portal) + } + if intent == nil { + intent = portal.MainIntent() } _, err := intent.RedactEvent(portal.MXID, msg.MXID) if err != nil { @@ -783,6 +785,11 @@ func (portal *Portal) HandleMessageRevoke(user *User, message whatsappExt.Messag msg.Delete() } +type MessageContent struct { + *mautrix.Content + IsCustomPuppet bool `json:"net.maunium.whatsapp.puppet,omitempty"` +} + func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessage) { if len(portal.MXID) == 0 { return @@ -808,7 +815,7 @@ func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessa portal.SetReply(content, message.Info) _, _ = intent.UserTyping(portal.MXID, false, 0) - resp, err := intent.SendMassagedMessageEvent(portal.MXID, mautrix.EventMessage, content, int64(message.Info.Timestamp*1000)) + resp, err := intent.SendMassagedMessageEvent(portal.MXID, mautrix.EventMessage, &MessageContent{content, intent.IsCustomPuppet}, int64(message.Info.Timestamp*1000)) if err != nil { portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) return @@ -891,7 +898,7 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, _, _ = intent.UserTyping(portal.MXID, false, 0) ts := int64(info.Timestamp * 1000) - resp, err := intent.SendMassagedMessageEvent(portal.MXID, mautrix.EventMessage, content, ts) + resp, err := intent.SendMassagedMessageEvent(portal.MXID, mautrix.EventMessage, &MessageContent{content, intent.IsCustomPuppet}, ts) if err != nil { portal.log.Errorfln("Failed to handle message %s: %v", info.Id, err) return @@ -905,7 +912,7 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, portal.bridge.Formatter.ParseWhatsApp(captionContent) - _, err := intent.SendMassagedMessageEvent(portal.MXID, mautrix.EventMessage, captionContent, ts) + _, err := intent.SendMassagedMessageEvent(portal.MXID, mautrix.EventMessage, &MessageContent{captionContent, intent.IsCustomPuppet}, ts) if err != nil { portal.log.Warnfln("Failed to handle caption of message %s: %v", info.Id, err) } @@ -1198,7 +1205,7 @@ func (portal *Portal) Cleanup(puppetsOnly bool) { } puppet := portal.bridge.GetPuppetByMXID(member) if puppet != nil { - _, err = puppet.Intent().LeaveRoom(portal.MXID) + _, err = puppet.DefaultIntent().LeaveRoom(portal.MXID) if err != nil { portal.log.Errorln("Error leaving as puppet while cleaning up portal:", err) } diff --git a/puppet.go b/puppet.go index e72919a..2c3c2d1 100644 --- a/puppet.go +++ b/puppet.go @@ -71,20 +71,49 @@ func (bridge *Bridge) GetPuppetByJID(jid types.WhatsAppID) *Puppet { } puppet = bridge.NewPuppet(dbPuppet) bridge.puppets[puppet.JID] = puppet + if len(puppet.CustomMXID) > 0 { + bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet + } } return puppet } -func (bridge *Bridge) GetAllPuppets() []*Puppet { +func (bridge *Bridge) GetPuppetByCustomMXID(mxid types.MatrixUserID) *Puppet { + bridge.puppetsLock.Lock() + defer bridge.puppetsLock.Unlock() + puppet, ok := bridge.puppetsByCustomMXID[mxid] + if !ok { + dbPuppet := bridge.DB.Puppet.GetByCustomMXID(mxid) + if dbPuppet == nil { + return nil + } + puppet = bridge.NewPuppet(dbPuppet) + bridge.puppets[puppet.JID] = puppet + bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet + } + return puppet +} + +func (bridge *Bridge) GetAllPuppetsWithCustomMXID() []*Puppet { + return bridge.dbPuppetsToPuppets(bridge.DB.Puppet.GetAllWithCustomMXID()) +} + +func (bridge *Bridge) GetAllPuppets() []*Puppet { + return bridge.dbPuppetsToPuppets(bridge.DB.Puppet.GetAll()) +} + +func (bridge *Bridge) dbPuppetsToPuppets(dbPuppets []*database.Puppet) []*Puppet { bridge.puppetsLock.Lock() defer bridge.puppetsLock.Unlock() - dbPuppets := bridge.DB.Puppet.GetAll() output := make([]*Puppet, len(dbPuppets)) for index, dbPuppet := range dbPuppets { puppet, ok := bridge.puppets[dbPuppet.JID] if !ok { puppet = bridge.NewPuppet(dbPuppet) bridge.puppets[dbPuppet.JID] = puppet + if len(dbPuppet.CustomMXID) > 0 { + bridge.puppetsByCustomMXID[dbPuppet.CustomMXID] = puppet + } } output[index] = puppet } @@ -116,13 +145,26 @@ type Puppet struct { typingAt int64 MXID types.MatrixUserID + + customIntent *appservice.IntentAPI } func (puppet *Puppet) PhoneNumber() string { return strings.Replace(puppet.JID, whatsappExt.NewUserSuffix, "", 1) } -func (puppet *Puppet) Intent() *appservice.IntentAPI { +func (puppet *Puppet) IntentFor(portal *Portal) *appservice.IntentAPI { + if puppet.customIntent == nil || portal.Key.JID == puppet.JID{ + return puppet.DefaultIntent() + } + return puppet.customIntent +} + +func (puppet *Puppet) CustomIntent() *appservice.IntentAPI { + return puppet.customIntent +} + +func (puppet *Puppet) DefaultIntent() *appservice.IntentAPI { return puppet.bridge.AS.Intent(puppet.MXID) } @@ -145,7 +187,7 @@ func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsappExt.ProfilePicI } if len(avatar.URL) == 0 { - err := puppet.Intent().SetAvatarURL("") + err := puppet.DefaultIntent().SetAvatarURL("") if err != nil { puppet.log.Warnln("Failed to remove avatar:", err) } @@ -160,13 +202,13 @@ func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsappExt.ProfilePicI } mime := http.DetectContentType(data) - resp, err := puppet.Intent().UploadBytes(data, mime) + resp, err := puppet.DefaultIntent().UploadBytes(data, mime) if err != nil { puppet.log.Warnln("Failed to upload avatar:", err) return false } - err = puppet.Intent().SetAvatarURL(resp.ContentURI) + err = puppet.DefaultIntent().SetAvatarURL(resp.ContentURI) if err != nil { puppet.log.Warnln("Failed to set avatar:", err) } @@ -175,7 +217,7 @@ func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsappExt.ProfilePicI } func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) { - err := puppet.Intent().EnsureRegistered() + err := puppet.DefaultIntent().EnsureRegistered() if err != nil { puppet.log.Errorln("Failed to ensure registered:", err) } @@ -185,7 +227,7 @@ func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) { } newName, quality := puppet.bridge.Config.Bridge.FormatDisplayname(contact) if puppet.Displayname != newName && quality >= puppet.NameQuality { - err := puppet.Intent().SetDisplayName(newName) + err := puppet.DefaultIntent().SetDisplayName(newName) if err == nil { puppet.Displayname = newName puppet.NameQuality = quality diff --git a/user.go b/user.go index d4916bd..7f9e69a 100644 --- a/user.go +++ b/user.go @@ -465,14 +465,15 @@ func (user *User) HandlePresence(info whatsappExt.Presence) { puppet := user.bridge.GetPuppetByJID(info.SenderJID) switch info.Status { case whatsappExt.PresenceUnavailable: - puppet.Intent().SetPresence("offline") + _ = puppet.DefaultIntent().SetPresence("offline") case whatsappExt.PresenceAvailable: if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() { - puppet.Intent().UserTyping(puppet.typingIn, false, 0) + portal := user.bridge.GetPortalByMXID(puppet.typingIn) + _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0) puppet.typingIn = "" puppet.typingAt = 0 } else { - puppet.Intent().SetPresence("online") + _ = puppet.DefaultIntent().SetPresence("online") } case whatsappExt.PresenceComposing: portal := user.GetPortalByJID(info.JID) @@ -480,11 +481,11 @@ func (user *User) HandlePresence(info whatsappExt.Presence) { if puppet.typingIn == portal.MXID { return } - puppet.Intent().UserTyping(puppet.typingIn, false, 0) + _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0) } puppet.typingIn = portal.MXID puppet.typingAt = time.Now().Unix() - puppet.Intent().UserTyping(portal.MXID, true, 15*1000) + _, _ = puppet.IntentFor(portal).UserTyping(portal.MXID, true, 15*1000) } } @@ -496,7 +497,7 @@ func (user *User) HandleMsgInfo(info whatsappExt.MsgInfo) { } go func() { - intent := user.bridge.GetPuppetByJID(info.SenderJID).Intent() + intent := user.bridge.GetPuppetByJID(info.SenderJID).IntentFor(portal) for _, id := range info.IDs { msg := user.bridge.DB.Message.GetByJID(portal.Key, id) if msg == nil {