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 {