Add basic Matrix puppeting support

May contain bugs.
EDUs from /sync are not yet handled.
This commit is contained in:
Tulir Asokan 2019-05-24 02:33:26 +03:00
parent 95e62fae77
commit 2c9c473040
14 changed files with 379 additions and 55 deletions

View file

@ -59,7 +59,7 @@
* [ ] When receiving invite<sup>[2]</sup>
* [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
<sup>[1]</sup> May involve reverse-engineering the WhatsApp Web API and/or editing go-whatsapp

View file

@ -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 <access token>`")
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) {
}

View file

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

168
custompuppet.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
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 }

View file

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

View file

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

View file

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

View file

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

6
go.mod
View file

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

24
main.go
View file

@ -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() {

View file

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

View file

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

View file

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

13
user.go
View file

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