diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0422d43..cab62cf 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -55,7 +55,7 @@ build docker amd64:
DOCKER_ARCH: amd64
after_script:
- |
- if [[ "$CI_COMMIT_BRANCH" == "master" && "$CI_JOB_STATUS" == "success" ]]; then
+ if [[ "$CI_COMMIT_BRANCH" == "legacy" && "$CI_JOB_STATUS" == "success" ]]; then
apk add --update curl jq
rm -rf /var/cache/apk/*
diff --git a/README.md b/README.md
index 8942729..1a7b521 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,10 @@
# mautrix-whatsapp
-A Matrix-WhatsApp puppeting bridge based on the [Rhymen/go-whatsapp](https://github.com/Rhymen/go-whatsapp)
-implementation of the [sigalor/whatsapp-web-reveng](https://github.com/sigalor/whatsapp-web-reveng) project.
+A Matrix-WhatsApp puppeting bridge based on [whatsmeow](https://github.com/tulir/whatsmeow).
### Documentation
-All setup and usage instructions are located on
-[docs.mau.fi](https://docs.mau.fi/bridges/go/whatsapp/index.html).
-Some quick links:
+All setup and usage instructions are located on [docs.mau.fi]. Some quick links:
+
+[docs.mau.fi]: https://docs.mau.fi/bridges/go/whatsapp/index.html
* [Bridge setup](https://docs.mau.fi/bridges/go/whatsapp/setup/index.html)
(or [with Docker](https://docs.mau.fi/bridges/go/whatsapp/setup/docker.html))
diff --git a/ROADMAP.md b/ROADMAP.md
index 3d9f8ac..9510492 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -12,14 +12,14 @@
* [x] Read receipts
* [ ] Power level
* [ ] Membership actions
- * [x] Invite
+ * [ ] Invite
* [ ] Join
- * [x] Leave
- * [x] Kick
+ * [ ] Leave
+ * [ ] Kick
* [ ] Room metadata changes
- * [x] Name
- * [ ] Avatar[1]
- * [x] Topic
+ * [ ] Name
+ * [ ] Avatar
+ * [ ] Topic
* [ ] Initial room metadata
* WhatsApp → Matrix
* [x] Message content
@@ -32,10 +32,10 @@
* [ ] Chat types
* [x] Private chat
* [x] Group chat
- * [ ] Broadcast list[2]
+ * [ ] Broadcast list
* [x] Message deletions
* [x] Avatars
- * [x] Presence
+ * [ ] Presence
* [x] Typing notifications
* [x] Read receipts
* [x] Admin/superadmin status
@@ -49,8 +49,8 @@
* [x] Avatar
* [x] Description
* [x] Initial group metadata
- * [ ] User metadata changes
- * [ ] Display name[3]
+ * [x] User metadata changes
+ * [x] Display name
* [x] Avatar
* [x] Initial user metadata
* [x] Display name
@@ -63,7 +63,3 @@
* [x] Private chat creation by inviting Matrix puppet of WhatsApp user to new room
* [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
-[2] May already work
-[3] May not be possible
diff --git a/bridgestate.go b/bridgestate.go
index 5b4d3b1..4f6b6eb 100644
--- a/bridgestate.go
+++ b/bridgestate.go
@@ -20,16 +20,11 @@ import (
"bytes"
"context"
"encoding/json"
- "errors"
"fmt"
"io/ioutil"
"net/http"
- "strings"
- "sync/atomic"
"time"
- "github.com/Rhymen/go-whatsapp"
-
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
@@ -38,7 +33,6 @@ import (
type BridgeStateEvent string
const (
- StateStarting BridgeStateEvent = "STARTING"
StateUnconfigured BridgeStateEvent = "UNCONFIGURED"
StateRunning BridgeStateEvent = "RUNNING"
StateConnecting BridgeStateEvent = "CONNECTING"
@@ -56,20 +50,14 @@ const (
WANotLoggedIn BridgeErrorCode = "wa-logged-out"
WANotConnected BridgeErrorCode = "wa-not-connected"
WAConnecting BridgeErrorCode = "wa-connecting"
- WATimeout BridgeErrorCode = "wa-timeout"
WAServerTimeout BridgeErrorCode = "wa-server-timeout"
- WAPingFalse BridgeErrorCode = "wa-ping-false"
- WAPingError BridgeErrorCode = "wa-ping-error"
)
var bridgeHumanErrors = map[BridgeErrorCode]string{
WANotLoggedIn: "You're not logged into WhatsApp",
WANotConnected: "You're not connected to WhatsApp",
WAConnecting: "Trying to reconnect to WhatsApp. Please make sure WhatsApp is running on your phone and connected to the internet.",
- WATimeout: "WhatsApp on your phone is not responding. Please make sure it is running and connected to the internet.",
WAServerTimeout: "The WhatsApp web servers are not responding. The bridge will try to reconnect.",
- WAPingFalse: "WhatsApp returned an error, reconnecting. Please make sure WhatsApp is running on your phone and connected to the internet.",
- WAPingError: "WhatsApp returned an unknown error",
}
type BridgeState struct {
@@ -94,8 +82,8 @@ type GlobalBridgeState struct {
func (pong BridgeState) fill(user *User) BridgeState {
if user != nil {
pong.UserID = user.MXID
- pong.RemoteID = strings.TrimSuffix(user.JID, whatsapp.NewUserSuffix)
- pong.RemoteName = fmt.Sprintf("+%s", pong.RemoteID)
+ pong.RemoteID = fmt.Sprintf("%s_a%d_d%d", user.JID.User, user.JID.Agent, user.JID.Device)
+ pong.RemoteName = fmt.Sprintf("+%s", user.JID.User)
}
pong.Timestamp = time.Now().Unix()
@@ -116,32 +104,6 @@ func (pong *BridgeState) shouldDeduplicate(newPong *BridgeState) bool {
return pong.Timestamp+int64(pong.TTL/5) > time.Now().Unix()
}
-func (user *User) setupAdminTestHooks() {
- if len(user.bridge.Config.Homeserver.StatusEndpoint) == 0 {
- return
- }
- user.Conn.AdminTestHook = func(err error) {
- if errors.Is(err, whatsapp.ErrConnectionTimeout) {
- user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WATimeout})
- } else if errors.Is(err, whatsapp.ErrWebsocketKeepaliveFailed) {
- user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAServerTimeout})
- } else if errors.Is(err, whatsapp.ErrPingFalse) {
- user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAPingFalse})
- } else if err == nil {
- user.sendBridgeState(BridgeState{StateEvent: StateConnected})
- } else {
- user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAPingError})
- }
- }
- user.Conn.CountTimeoutHook = func(wsKeepaliveErrorCount int) {
- if wsKeepaliveErrorCount > 0 {
- user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAServerTimeout})
- } else {
- user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WATimeout})
- }
- }
-}
-
func (bridge *Bridge) createBridgeStateRequest(ctx context.Context, state *BridgeState) (req *http.Request, err error) {
var body bytes.Buffer
if err = json.NewEncoder(&body).Encode(&state); err != nil {
@@ -210,8 +172,6 @@ func (user *User) sendBridgeState(state BridgeState) {
}
}
-var bridgeStatePingID uint32 = 0
-
func (prov *ProvisioningAPI) BridgeStatePing(w http.ResponseWriter, r *http.Request) {
if !prov.bridge.AS.CheckServerToken(w, r) {
return
@@ -221,37 +181,12 @@ func (prov *ProvisioningAPI) BridgeStatePing(w http.ResponseWriter, r *http.Requ
var global BridgeState
global.StateEvent = StateRunning
var remote BridgeState
- if user.Conn != nil {
- if user.Conn.IsConnected() && user.Conn.IsLoggedIn() {
- pingID := atomic.AddUint32(&bridgeStatePingID, 1)
- user.log.Debugfln("Pinging WhatsApp mobile due to bridge status /ping API request (ID %d)", pingID)
- err := user.Conn.AdminTestWithSuppress(true)
- if errors.Is(r.Context().Err(), context.Canceled) {
- user.log.Warnfln("Ping request %d was canceled before we responded (response was %v)", pingID, err)
- user.prevBridgeStatus = nil
- return
- }
- user.log.Debugfln("Ping %d response: %v", pingID, err)
- remote.StateEvent = StateTransientDisconnect
- if err == whatsapp.ErrPingFalse {
- user.log.Debugln("Forwarding ping false error from provisioning API to HandleError")
- go user.HandleError(err)
- remote.Error = WAPingFalse
- } else if errors.Is(err, whatsapp.ErrConnectionTimeout) {
- remote.Error = WATimeout
- } else if errors.Is(err, whatsapp.ErrWebsocketKeepaliveFailed) {
- remote.Error = WAServerTimeout
- } else if err != nil {
- remote.Error = WAPingError
- } else {
- remote.StateEvent = StateConnected
- }
- } else if user.Conn.IsLoginInProgress() && user.Session != nil {
+ if user.IsConnected() {
+ if user.Client.IsLoggedIn {
+ remote.StateEvent = StateConnected
+ } else if user.Session != nil {
remote.StateEvent = StateConnecting
remote.Error = WAConnecting
- } else if !user.Conn.IsConnected() && user.Session != nil {
- remote.StateEvent = StateBadCredentials
- remote.Error = WANotConnected
} // else: unconfigured
} else if user.Session != nil {
remote.StateEvent = StateBadCredentials
diff --git a/commands.go b/commands.go
index bef9bf7..741d96a 100644
--- a/commands.go
+++ b/commands.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -20,22 +20,21 @@ import (
"context"
"errors"
"fmt"
- "math"
- "sort"
"strconv"
"strings"
- "github.com/Rhymen/go-whatsapp"
+ "github.com/skip2/go-qrcode"
"maunium.net/go/maulogger/v2"
+ "go.mau.fi/whatsmeow"
+ "go.mau.fi/whatsmeow/types"
+
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
-
- "maunium.net/go/mautrix-whatsapp/database"
)
type CommandHandler struct {
@@ -94,17 +93,11 @@ func (handler *CommandHandler) Handle(roomID id.RoomID, user *User, message stri
Args: args[1:],
}
handler.log.Debugfln("%s sent '%s' in %s", user.MXID, message, roomID)
- if roomID == handler.bridge.Config.Bridge.Relaybot.ManagementRoom {
- handler.CommandRelaybot(ce)
- } else {
- handler.CommandMux(ce)
- }
+ handler.CommandMux(ce)
}
func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
switch ce.Command {
- case "relaybot":
- handler.CommandRelaybot(ce)
case "login":
handler.CommandLogin(ce)
case "logout-matrix":
@@ -119,8 +112,6 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
handler.CommandDisconnect(ce)
case "ping":
handler.CommandPing(ce)
- case "delete-connection":
- handler.CommandDeleteConnection(ce)
case "delete-session":
handler.CommandDeleteSession(ce)
case "delete-portal":
@@ -137,20 +128,22 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
handler.CommandLogout(ce)
case "toggle":
handler.CommandToggle(ce)
- case "login-matrix", "sync", "list", "open", "pm", "invite-link", "join", "create":
+ case "set-relay", "unset-relay", "login-matrix", "sync", "list", "open", "pm", "invite-link", "join", "create":
if !ce.User.HasSession() {
ce.Reply("You are not logged in. Use the `login` command to log into WhatsApp.")
return
- } else if !ce.User.IsConnected() {
+ } else if !ce.User.IsLoggedIn() {
ce.Reply("You are not connected to WhatsApp. Use the `reconnect` command to reconnect.")
return
}
switch ce.Command {
+ case "set-relay":
+ handler.CommandSetRelay(ce)
+ case "unset-relay":
+ handler.CommandUnsetRelay(ce)
case "login-matrix":
handler.CommandLoginMatrix(ce)
- case "sync":
- handler.CommandSync(ce)
case "list":
handler.CommandList(ce)
case "open":
@@ -180,22 +173,35 @@ func (handler *CommandHandler) CommandDiscardMegolmSession(ce *CommandEvent) {
}
}
-func (handler *CommandHandler) CommandRelaybot(ce *CommandEvent) {
- if handler.bridge.Relaybot == nil {
- ce.Reply("The relaybot is disabled")
- } else if !ce.User.Admin {
- ce.Reply("Only admins can manage the relaybot")
+const cmdSetRelayHelp = `set-relay - Relay messages in this room through your WhatsApp account.`
+
+func (handler *CommandHandler) CommandSetRelay(ce *CommandEvent) {
+ if !handler.bridge.Config.Bridge.Relay.Enabled {
+ ce.Reply("Relay mode is not enabled on this instance of the bridge")
+ } else if ce.Portal == nil {
+ ce.Reply("This is not a portal room")
+ } else if handler.bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin {
+ ce.Reply("Only admins are allowed to enable relay mode on this instance of the bridge")
} else {
- if ce.Command == "relaybot" {
- if len(ce.Args) == 0 {
- ce.Reply("**Usage:** `relaybot `")
- return
- }
- ce.Command = strings.ToLower(ce.Args[0])
- ce.Args = ce.Args[1:]
- }
- ce.User = handler.bridge.Relaybot
- handler.CommandMux(ce)
+ ce.Portal.RelayUserID = ce.User.MXID
+ ce.Portal.Update()
+ ce.Reply("Messages from non-logged-in users in this room will now be bridged through your WhatsApp account")
+ }
+}
+
+const cmdUnsetRelayHelp = `set-relay - Stop relaying messages in this room.`
+
+func (handler *CommandHandler) CommandUnsetRelay(ce *CommandEvent) {
+ if !handler.bridge.Config.Bridge.Relay.Enabled {
+ ce.Reply("Relay mode is not enabled on this instance of the bridge")
+ } else if ce.Portal == nil {
+ ce.Reply("This is not a portal room")
+ } else if handler.bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin {
+ ce.Reply("Only admins are allowed to enable relay mode on this instance of the bridge")
+ } else {
+ ce.Portal.RelayUserID = ""
+ ce.Portal.Update()
+ ce.Reply("Messages from non-logged-in users will no longer be bridged in this room")
}
}
@@ -226,12 +232,14 @@ func (handler *CommandHandler) CommandInviteLink(ce *CommandEvent) {
return
}
- link, err := ce.User.Conn.GroupInviteLink(ce.Portal.Key.JID)
- if err != nil {
- ce.Reply("Failed to get invite link: %v", err)
- return
- }
- ce.Reply("%s%s", inviteLinkPrefix, link)
+ ce.Reply("Not yet implemented")
+ // TODO reimplement
+ //link, err := ce.User.Conn.GroupInviteLink(ce.Portal.Key.JID)
+ //if err != nil {
+ // ce.Reply("Failed to get invite link: %v", err)
+ // return
+ //}
+ //ce.Reply("%s%s", inviteLinkPrefix, link)
}
const cmdJoinHelp = `join - Join a group chat with an invite link.`
@@ -246,26 +254,28 @@ func (handler *CommandHandler) CommandJoin(ce *CommandEvent) {
return
}
- jid, err := ce.User.Conn.GroupAcceptInviteCode(ce.Args[0][len(inviteLinkPrefix):])
- if err != nil {
- ce.Reply("Failed to join group: %v", err)
- return
- }
-
- handler.log.Debugln("%s successfully joined group %s", ce.User.MXID, jid)
- portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(jid))
- if len(portal.MXID) > 0 {
- portal.Sync(ce.User, whatsapp.Contact{JID: portal.Key.JID})
- ce.Reply("Successfully joined group \"%s\" and synced portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
- } else {
- err = portal.CreateMatrixRoom(ce.User)
- if err != nil {
- ce.Reply("Failed to create portal room: %v", err)
- return
- }
-
- ce.Reply("Successfully joined group \"%s\" and created portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
- }
+ ce.Reply("Not yet implemented")
+ // TODO reimplement
+ //jid, err := ce.User.Conn.GroupAcceptInviteCode(ce.Args[0][len(inviteLinkPrefix):])
+ //if err != nil {
+ // ce.Reply("Failed to join group: %v", err)
+ // return
+ //}
+ //
+ //handler.log.Debugln("%s successfully joined group %s", ce.User.MXID, jid)
+ //portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(jid))
+ //if len(portal.MXID) > 0 {
+ // portal.Sync(ce.User, whatsapp.Contact{JID: portal.Key.JID})
+ // ce.Reply("Successfully joined group \"%s\" and synced portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
+ //} else {
+ // err = portal.CreateMatrixRoom(ce.User)
+ // if err != nil {
+ // ce.Reply("Failed to create portal room: %v", err)
+ // return
+ // }
+ //
+ // ce.Reply("Successfully joined group \"%s\" and created portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
+ //}
}
const cmdCreateHelp = `create - Create a group chat.`
@@ -299,43 +309,45 @@ func (handler *CommandHandler) CommandCreate(ce *CommandEvent) {
return
}
- participants := []string{ce.User.JID}
+ participants := []types.JID{ce.User.JID.ToNonAD()}
for userID := range members.Joined {
jid, ok := handler.bridge.ParsePuppetMXID(userID)
- if ok && jid != ce.User.JID {
+ if ok && jid.User != ce.User.JID.User {
participants = append(participants, jid)
}
}
- resp, err := ce.User.Conn.CreateGroup(roomNameEvent.Name, participants)
- if err != nil {
- ce.Reply("Failed to create group: %v", err)
- return
- }
- portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(resp.GroupID))
- portal.roomCreateLock.Lock()
- defer portal.roomCreateLock.Unlock()
- if len(portal.MXID) != 0 {
- portal.log.Warnln("Detected race condition in room creation")
- // TODO race condition, clean up the old room
- }
- portal.MXID = ce.RoomID
- portal.Name = roomNameEvent.Name
- portal.Encrypted = encryptionEvent.Algorithm == id.AlgorithmMegolmV1
- if !portal.Encrypted && handler.bridge.Config.Bridge.Encryption.Default {
- _, err = portal.MainIntent().SendStateEvent(portal.MXID, event.StateEncryption, "", &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1})
- if err != nil {
- portal.log.Warnln("Failed to enable e2be:", err)
- }
- portal.Encrypted = true
- }
-
- portal.Update()
- portal.UpdateBridgeInfo()
-
- ce.Reply("Successfully created WhatsApp group %s", portal.Key.JID)
- inCommunity := ce.User.addPortalToCommunity(portal)
- ce.User.CreateUserPortal(database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity})
+ ce.Reply("Not yet implemented")
+ // TODO reimplement
+ //resp, err := ce.User.Conn.CreateGroup(roomNameEvent.Name, participants)
+ //if err != nil {
+ // ce.Reply("Failed to create group: %v", err)
+ // return
+ //}
+ //portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(resp.GroupID))
+ //portal.roomCreateLock.Lock()
+ //defer portal.roomCreateLock.Unlock()
+ //if len(portal.MXID) != 0 {
+ // portal.log.Warnln("Detected race condition in room creation")
+ // // TODO race condition, clean up the old room
+ //}
+ //portal.MXID = ce.RoomID
+ //portal.Name = roomNameEvent.Name
+ //portal.Encrypted = encryptionEvent.Algorithm == id.AlgorithmMegolmV1
+ //if !portal.Encrypted && handler.bridge.Config.Bridge.Encryption.Default {
+ // _, err = portal.MainIntent().SendStateEvent(portal.MXID, event.StateEncryption, "", &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1})
+ // if err != nil {
+ // portal.log.Warnln("Failed to enable e2be:", err)
+ // }
+ // portal.Encrypted = true
+ //}
+ //
+ //portal.Update()
+ //portal.UpdateBridgeInfo()
+ //
+ //ce.Reply("Successfully created WhatsApp group %s", portal.Key.JID)
+ //inCommunity := ce.User.addPortalToCommunity(portal)
+ //ce.User.CreateUserPortal(database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity})
}
const cmdSetPowerLevelHelp = `set-pl [user ID] - Change the power level in a portal room. Only for bridge admins.`
@@ -378,25 +390,94 @@ func (handler *CommandHandler) CommandSetPowerLevel(ce *CommandEvent) {
}
}
-const cmdLoginHelp = `login - Authenticate this Bridge as WhatsApp Web Client`
+const cmdLoginHelp = `login - Link the bridge to your WhatsApp account as a web client`
// CommandLogin handles login command
func (handler *CommandHandler) CommandLogin(ce *CommandEvent) {
- if !ce.User.Connect(true) {
- ce.User.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.")
+ if ce.User.Session != nil {
+ if ce.User.IsConnected() {
+ ce.Reply("You're already logged in")
+ } else {
+ ce.Reply("You're already logged in. Perhaps you wanted to `reconnect`?")
+ }
return
}
- ce.User.Login(ce)
+
+ qrChan, err := ce.User.Login(context.Background())
+ if err != nil {
+ ce.User.log.Errorf("Failed to log in:", err)
+ ce.Reply("Failed to log in: %v", err)
+ return
+ }
+
+ var qrEventID id.EventID
+ for item := range qrChan {
+ switch item {
+ case whatsmeow.QRChannelSuccess:
+ jid := ce.User.Client.Store.ID
+ ce.Reply("Successfully logged in as +%s (device #%d)", jid.User, jid.Device)
+ case whatsmeow.QRChannelTimeout:
+ ce.Reply("QR code timed out. Please restart the login.")
+ case whatsmeow.QRChannelErrUnexpectedEvent:
+ ce.Reply("Failed to log in: unexpected connection event from server")
+ case whatsmeow.QRChannelScannedWithoutMultidevice:
+ ce.Reply("Please enable the WhatsApp multidevice beta and scan the QR code again.")
+ default:
+ qrEventID = ce.User.sendQR(ce, string(item), qrEventID)
+ }
+ }
+ _, _ = ce.Bot.RedactEvent(ce.RoomID, qrEventID)
}
-const cmdLogoutHelp = `logout - Logout from WhatsApp`
+func (user *User) sendQR(ce *CommandEvent, code string, prevEvent id.EventID) id.EventID {
+ url, ok := user.uploadQR(ce, code)
+ if !ok {
+ return prevEvent
+ }
+ content := event.MessageEventContent{
+ MsgType: event.MsgImage,
+ Body: code,
+ URL: url.CUString(),
+ }
+ if len(prevEvent) != 0 {
+ content.SetEdit(prevEvent)
+ }
+ resp, err := ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &content)
+ if err != nil {
+ user.log.Errorln("Failed to send edited QR code to user:", err)
+ } else if len(prevEvent) == 0 {
+ prevEvent = resp.EventID
+ }
+ return prevEvent
+}
+
+func (user *User) uploadQR(ce *CommandEvent, code string) (id.ContentURI, bool) {
+ qrCode, err := qrcode.Encode(code, qrcode.Low, 256)
+ if err != nil {
+ user.log.Errorln("Failed to encode QR code:", err)
+ ce.Reply("Failed to encode QR code: %v", err)
+ return id.ContentURI{}, false
+ }
+
+ bot := user.bridge.AS.BotClient()
+
+ resp, err := bot.UploadBytes(qrCode, "image/png")
+ if err != nil {
+ user.log.Errorln("Failed to upload QR code:", err)
+ ce.Reply("Failed to upload QR code: %v", err)
+ return id.ContentURI{}, false
+ }
+ return resp.ContentURI, true
+}
+
+const cmdLogoutHelp = `logout - Unlink the bridge from your WhatsApp account`
// CommandLogout handles !logout command
func (handler *CommandHandler) CommandLogout(ce *CommandEvent) {
if ce.User.Session == nil {
ce.Reply("You're not logged in.")
return
- } else if !ce.User.IsConnected() {
+ } else if !ce.User.IsLoggedIn() {
ce.Reply("You are not connected to WhatsApp. Use the `reconnect` command to reconnect, or `delete-session` to forget all login information.")
return
}
@@ -407,17 +488,16 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) {
ce.User.log.Warnln("Failed to logout-matrix while logging out of WhatsApp:", err)
}
}
- err := ce.User.Conn.Logout()
+ err := ce.User.Client.Logout()
if err != nil {
ce.User.log.Warnln("Error while logging out:", err)
ce.Reply("Unknown error while logging out: %v", err)
return
}
+ ce.User.Session = nil
ce.User.removeFromJIDMap(StateLoggedOut)
- // TODO this causes a foreign key violation, which should be fixed
- //ce.User.JID = ""
- ce.User.SetSession(nil)
ce.User.DeleteConnection()
+ ce.User.DeleteSession()
ce.Reply("Logged out successfully.")
}
@@ -439,16 +519,16 @@ func (handler *CommandHandler) CommandToggle(ce *CommandEvent) {
}
if ce.Args[0] == "presence" || ce.Args[0] == "all" {
customPuppet.EnablePresence = !customPuppet.EnablePresence
- var newPresence whatsapp.Presence
+ var newPresence types.Presence
if customPuppet.EnablePresence {
- newPresence = whatsapp.PresenceAvailable
+ newPresence = types.PresenceAvailable
ce.Reply("Enabled presence bridging")
} else {
- newPresence = whatsapp.PresenceUnavailable
+ newPresence = types.PresenceUnavailable
ce.Reply("Disabled presence bridging")
}
- if ce.User.IsConnected() {
- _, err := ce.User.Conn.Presence("", newPresence)
+ if ce.User.IsLoggedIn() {
+ err := ce.User.Client.SendPresence(newPresence)
if err != nil {
ce.User.log.Warnln("Failed to set presence:", err)
}
@@ -468,130 +548,96 @@ func (handler *CommandHandler) CommandToggle(ce *CommandEvent) {
const cmdDeleteSessionHelp = `delete-session - Delete session information and disconnect from WhatsApp without sending a logout request`
func (handler *CommandHandler) CommandDeleteSession(ce *CommandEvent) {
- if ce.User.Session == nil && ce.User.Conn == nil {
+ if ce.User.Session == nil && ce.User.Client == nil {
ce.Reply("Nothing to purge: no session information stored and no active connection.")
return
}
- //ce.User.JID = ""
ce.User.removeFromJIDMap(StateLoggedOut)
- ce.User.SetSession(nil)
ce.User.DeleteConnection()
+ ce.User.DeleteSession()
ce.Reply("Session information purged")
}
const cmdReconnectHelp = `reconnect - Reconnect to WhatsApp`
func (handler *CommandHandler) CommandReconnect(ce *CommandEvent) {
- if ce.User.Conn == nil {
- if ce.User.Session == nil {
- ce.Reply("No existing connection and no session. Did you mean `login`?")
- } else {
- ce.Reply("No existing connection, creating one...")
- ce.User.Connect(false)
- }
- return
- }
-
- wasConnected := true
- err := ce.User.Conn.Disconnect()
- if err == whatsapp.ErrNotConnected {
- wasConnected = false
- } else if err != nil {
- ce.User.log.Warnln("Error while disconnecting:", err)
- }
-
- ctx := context.Background()
-
- err = ce.User.Conn.Restore(true, ctx)
- if err == whatsapp.ErrInvalidSession {
- if ce.User.Session != nil {
- ce.User.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
- ce.User.Conn.SetSession(*ce.User.Session)
- err = ce.User.Conn.Restore(true, ctx)
- } else {
- ce.Reply("You are not logged in.")
- return
- }
- } else if err == whatsapp.ErrLoginInProgress {
- ce.Reply("A login or reconnection is already in progress.")
- return
- } else if err == whatsapp.ErrAlreadyLoggedIn {
- ce.Reply("You were already connected.")
- return
- }
- if err != nil {
- ce.User.log.Warnln("Error while reconnecting:", err)
- ce.Reply("Unknown error while reconnecting: %v", err)
- ce.User.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
- err = ce.User.Conn.Disconnect()
- if err != nil {
- ce.User.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
- }
- return
- }
- ce.User.ConnectionErrors = 0
-
- var msg string
- if wasConnected {
- msg = "Reconnected successfully."
- } else {
- msg = "Connected successfully."
- }
- ce.Reply(msg)
- ce.User.PostLogin()
-}
-
-const cmdDeleteConnectionHelp = `delete-connection - Disconnect ignoring errors and delete internal connection state.`
-
-func (handler *CommandHandler) CommandDeleteConnection(ce *CommandEvent) {
- if ce.User.Conn == nil {
- ce.Reply("You don't have a WhatsApp connection.")
- return
- }
- ce.User.DeleteConnection()
- ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.")
+ ce.Reply("Not yet implemented")
+ // TODO reimplement
+ //if ce.User.Client == nil {
+ // if ce.User.Session == nil {
+ // ce.Reply("No existing connection and no session. Did you mean `login`?")
+ // } else {
+ // ce.Reply("No existing connection, creating one...")
+ // ce.User.Connect(false)
+ // }
+ // return
+ //}
+ //
+ //wasConnected := true
+ //ce.User.Client.Disconnect()
+ //ctx := context.Background()
+ //connected := ce.User.Connect(false)
+ //
+ //err = ce.User.Conn.Restore(true, ctx)
+ //if err == whatsapp.ErrInvalidSession {
+ // if ce.User.Session != nil {
+ // ce.User.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
+ // ce.User.Conn.SetSession(*ce.User.Session)
+ // err = ce.User.Conn.Restore(true, ctx)
+ // } else {
+ // ce.Reply("You are not logged in.")
+ // return
+ // }
+ //} else if err == whatsapp.ErrLoginInProgress {
+ // ce.Reply("A login or reconnection is already in progress.")
+ // return
+ //} else if err == whatsapp.ErrAlreadyLoggedIn {
+ // ce.Reply("You were already connected.")
+ // return
+ //}
+ //if err != nil {
+ // ce.User.log.Warnln("Error while reconnecting:", err)
+ // ce.Reply("Unknown error while reconnecting: %v", err)
+ // ce.User.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
+ // err = ce.User.Conn.Disconnect()
+ // if err != nil {
+ // ce.User.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
+ // }
+ // return
+ //}
+ //ce.User.ConnectionErrors = 0
+ //
+ //var msg string
+ //if wasConnected {
+ // msg = "Reconnected successfully."
+ //} else {
+ // msg = "Connected successfully."
+ //}
+ //ce.Reply(msg)
+ //ce.User.PostLogin()
}
const cmdDisconnectHelp = `disconnect - Disconnect from WhatsApp (without logging out)`
func (handler *CommandHandler) CommandDisconnect(ce *CommandEvent) {
- if ce.User.Conn == nil {
+ if ce.User.Client == nil {
ce.Reply("You don't have a WhatsApp connection.")
return
}
- err := ce.User.Conn.Disconnect()
- if err == whatsapp.ErrNotConnected {
- ce.Reply("You were not connected.")
- return
- } else if err != nil {
- ce.User.log.Warnln("Error while disconnecting:", err)
- ce.Reply("Unknown error while disconnecting: %v", err)
- return
- }
- ce.User.bridge.Metrics.TrackConnectionState(ce.User.JID, false)
- ce.User.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
+ ce.User.DeleteConnection()
ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.")
+ ce.User.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
}
const cmdPingHelp = `ping - Check your connection to WhatsApp.`
func (handler *CommandHandler) CommandPing(ce *CommandEvent) {
if ce.User.Session == nil {
- if ce.User.IsLoginInProgress() {
- ce.Reply("You're not logged into WhatsApp, but there's a login in progress.")
- } else {
- ce.Reply("You're not logged into WhatsApp.")
- }
- } else if ce.User.Conn == nil {
+ ce.Reply("You're not logged into WhatsApp.")
+ } else if ce.User.Client == nil || !ce.User.Client.IsConnected() {
ce.Reply("You don't have a WhatsApp connection.")
- } else if err := ce.User.Conn.AdminTest(); err != nil {
- if ce.User.IsLoginInProgress() {
- ce.Reply("Connection not OK: %v, but login in progress", err)
- } else {
- ce.Reply("Connection not OK: %v", err)
- }
} else {
- ce.Reply("Connection to WhatsApp OK")
+ ce.Reply("Connection to WhatsApp OK (probably)")
}
}
@@ -600,7 +646,7 @@ const cmdHelpHelp = `help - Prints this help`
// CommandHelp handles help command
func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
cmdPrefix := ""
- if ce.User.ManagementRoom != ce.RoomID || ce.User.IsRelaybot {
+ if ce.User.ManagementRoom != ce.RoomID {
cmdPrefix = handler.bridge.Config.Bridge.CommandPrefix + " "
}
@@ -612,12 +658,12 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
cmdPrefix + cmdDeleteSessionHelp,
cmdPrefix + cmdReconnectHelp,
cmdPrefix + cmdDisconnectHelp,
- cmdPrefix + cmdDeleteConnectionHelp,
cmdPrefix + cmdPingHelp,
+ cmdPrefix + cmdSetRelayHelp,
+ cmdPrefix + cmdUnsetRelayHelp,
cmdPrefix + cmdLoginMatrixHelp,
cmdPrefix + cmdLogoutMatrixHelp,
cmdPrefix + cmdToggleHelp,
- cmdPrefix + cmdSyncHelp,
cmdPrefix + cmdListHelp,
cmdPrefix + cmdOpenHelp,
cmdPrefix + cmdPMHelp,
@@ -630,35 +676,23 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
}, "\n* "))
}
-const cmdSyncHelp = `sync [--create-all] - Synchronize contacts from phone and optionally create portals for group chats.`
-
-// CommandSync handles sync command
-func (handler *CommandHandler) CommandSync(ce *CommandEvent) {
- user := ce.User
- create := len(ce.Args) > 0 && ce.Args[0] == "--create-all"
-
- ce.Reply("Updating contact and chat list...")
- handler.log.Debugln("Importing contacts of", user.MXID)
- _, err := user.Conn.Contacts()
+func canDeletePortal(portal *Portal, userID id.UserID) bool {
+ members, err := portal.MainIntent().JoinedMembers(portal.MXID)
if err != nil {
- user.log.Errorln("Error updating contacts:", err)
- ce.Reply("Failed to sync contact list (see logs for details)")
- return
+ portal.log.Errorfln("Failed to get joined members to check if portal can be deleted by %s: %v", userID, err)
+ return false
}
- handler.log.Debugln("Importing chats of", user.MXID)
- _, err = user.Conn.Chats()
- if err != nil {
- user.log.Errorln("Error updating chats:", err)
- ce.Reply("Failed to sync chat list (see logs for details)")
- return
+ for otherUser := range members.Joined {
+ _, isPuppet := portal.bridge.ParsePuppetMXID(otherUser)
+ if isPuppet || otherUser == portal.bridge.Bot.UserID || otherUser == userID {
+ continue
+ }
+ user := portal.bridge.GetUserByMXID(otherUser)
+ if user != nil && user.Session != nil {
+ return false
+ }
}
-
- ce.Reply("Syncing contacts...")
- user.syncPuppets(nil)
- ce.Reply("Syncing chats...")
- user.syncPortals(nil, create)
-
- ce.Reply("Sync complete.")
+ return true
}
const cmdDeletePortalHelp = `delete-portal - Delete the current portal. If the portal is used by other people, this is limited to bridge admins.`
@@ -669,12 +703,9 @@ func (handler *CommandHandler) CommandDeletePortal(ce *CommandEvent) {
return
}
- if !ce.User.Admin {
- users := ce.Portal.GetUserIDs()
- if len(users) > 1 || (len(users) == 1 && users[0] != ce.User.MXID) {
- ce.Reply("Only bridge admins can delete portals with other Matrix users")
- return
- }
+ if !ce.User.Admin && !canDeletePortal(ce.Portal, ce.User.MXID) {
+ ce.Reply("Only bridge admins can delete portals with other Matrix users")
+ return
}
ce.Portal.log.Infoln(ce.User.MXID, "requested deletion of portal.")
@@ -682,17 +713,23 @@ func (handler *CommandHandler) CommandDeletePortal(ce *CommandEvent) {
ce.Portal.Cleanup(false)
}
-const cmdDeleteAllPortalsHelp = `delete-all-portals - Delete all your portals that aren't used by any other user.'`
+const cmdDeleteAllPortalsHelp = `delete-all-portals - Delete all portals.`
func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) {
- portals := ce.User.GetPortals()
- portalsToDelete := make([]*Portal, 0, len(portals))
- for _, portal := range portals {
- users := portal.GetUserIDs()
- if len(users) == 1 && users[0] == ce.User.MXID {
- portalsToDelete = append(portalsToDelete, portal)
+ portals := handler.bridge.GetAllPortals()
+ var portalsToDelete []*Portal
+
+ if ce.User.Admin {
+ portals = portalsToDelete
+ } else {
+ portalsToDelete = portals[:0]
+ for _, portal := range portals {
+ if canDeletePortal(portal, ce.User.MXID) {
+ portalsToDelete = append(portalsToDelete, portal)
+ }
}
}
+
leave := func(portal *Portal) {
if len(portal.MXID) > 0 {
_, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{
@@ -711,13 +748,12 @@ func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) {
}
}
}
- ce.Reply("Found %d portals with no other users, deleting...", len(portalsToDelete))
+ ce.Reply("Found %d portals, deleting...", len(portalsToDelete))
for _, portal := range portalsToDelete {
portal.Delete()
leave(portal)
}
- ce.Reply("Finished deleting portal info. Now cleaning up rooms in background. " +
- "You may already continue using the bridge. Use `sync` to recreate portals.")
+ ce.Reply("Finished deleting portal info. Now cleaning up rooms in background.")
go func() {
for _, portal := range portalsToDelete {
@@ -729,21 +765,21 @@ func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) {
const cmdListHelp = `list [page] [items per page] - Get a list of all contacts and groups.`
-func formatContacts(contacts bool, input map[string]whatsapp.Contact) (result []string) {
- for jid, contact := range input {
- if strings.HasSuffix(jid, whatsapp.NewUserSuffix) != contacts {
- continue
- }
-
- if contacts {
- result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.JID[:len(contact.JID)-len(whatsapp.NewUserSuffix)]))
- } else {
- result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.JID))
- }
- }
- sort.Sort(sort.StringSlice(result))
- return
-}
+//func formatContacts(contacts bool, input map[string]whatsapp.Contact) (result []string) {
+// for jid, contact := range input {
+// if strings.HasSuffix(jid, whatsapp.NewUserSuffix) != contacts {
+// continue
+// }
+//
+// if contacts {
+// result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.JID[:len(contact.JID)-len(whatsapp.NewUserSuffix)]))
+// } else {
+// result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.JID))
+// }
+// }
+// sort.Sort(sort.StringSlice(result))
+// return
+//}
func (handler *CommandHandler) CommandList(ce *CommandEvent) {
if len(ce.Args) == 0 {
@@ -774,33 +810,35 @@ func (handler *CommandHandler) CommandList(ce *CommandEvent) {
ce.Reply("Warning: a high number of items per page may fail to send a reply")
}
}
- contacts := mode[0] == 'c'
- typeName := "Groups"
- if contacts {
- typeName = "Contacts"
- }
- ce.User.Conn.Store.ContactsLock.RLock()
- result := formatContacts(contacts, ce.User.Conn.Store.Contacts)
- ce.User.Conn.Store.ContactsLock.RUnlock()
- if len(result) == 0 {
- ce.Reply("No %s found", strings.ToLower(typeName))
- return
- }
- pages := int(math.Ceil(float64(len(result)) / float64(max)))
- if (page-1)*max >= len(result) {
- if pages == 1 {
- ce.Reply("There is only 1 page of %s", strings.ToLower(typeName))
- } else {
- ce.Reply("There are only %d pages of %s", pages, strings.ToLower(typeName))
- }
- return
- }
- lastIndex := page * max
- if lastIndex > len(result) {
- lastIndex = len(result)
- }
- result = result[(page-1)*max : lastIndex]
- ce.Reply("### %s (page %d of %d)\n\n%s", typeName, page, pages, strings.Join(result, "\n"))
+ ce.Reply("Not yet implemented")
+ // TODO reimplement
+ //contacts := mode[0] == 'c'
+ //typeName := "Groups"
+ //if contacts {
+ // typeName = "Contacts"
+ //}
+ //ce.User.Conn.Store.ContactsLock.RLock()
+ //result := formatContacts(contacts, ce.User.Conn.Store.Contacts)
+ //ce.User.Conn.Store.ContactsLock.RUnlock()
+ //if len(result) == 0 {
+ // ce.Reply("No %s found", strings.ToLower(typeName))
+ // return
+ //}
+ //pages := int(math.Ceil(float64(len(result)) / float64(max)))
+ //if (page-1)*max >= len(result) {
+ // if pages == 1 {
+ // ce.Reply("There is only 1 page of %s", strings.ToLower(typeName))
+ // } else {
+ // ce.Reply("There are only %d pages of %s", pages, strings.ToLower(typeName))
+ // }
+ // return
+ //}
+ //lastIndex := page * max
+ //if lastIndex > len(result) {
+ // lastIndex = len(result)
+ //}
+ //result = result[(page-1)*max : lastIndex]
+ //ce.Reply("### %s (page %d of %d)\n\n%s", typeName, page, pages, strings.Join(result, "\n"))
}
const cmdOpenHelp = `open <_group JID_> - Open a group chat portal.`
@@ -810,91 +848,75 @@ func (handler *CommandHandler) CommandOpen(ce *CommandEvent) {
ce.Reply("**Usage:** `open `")
return
}
+ ce.Reply("Not yet implemented")
- user := ce.User
- jid := ce.Args[0]
-
- if strings.HasSuffix(jid, whatsapp.NewUserSuffix) {
- ce.Reply("That looks like a user JID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsapp.NewUserSuffix)])
- return
- }
-
- user.Conn.Store.ContactsLock.RLock()
- contact, ok := user.Conn.Store.Contacts[jid]
- user.Conn.Store.ContactsLock.RUnlock()
- if !ok {
- ce.Reply("Group JID not found in contacts. Try syncing contacts with `sync` first.")
- return
- }
- handler.log.Debugln("Importing", jid, "for", user)
- portal := user.bridge.GetPortalByJID(database.GroupPortalKey(jid))
- if len(portal.MXID) > 0 {
- portal.Sync(user, contact)
- ce.Reply("Portal room synced.")
- } else {
- portal.Sync(user, contact)
- ce.Reply("Portal room created.")
- }
- _, _ = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID})
+ // TODO reimplement
+ //user := ce.User
+ //jid := ce.Args[0]
+ //if strings.HasSuffix(jid, whatsapp.NewUserSuffix) {
+ // ce.Reply("That looks like a user JID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsapp.NewUserSuffix)])
+ // return
+ //}
+ //
+ //user.Conn.Store.ContactsLock.RLock()
+ //contact, ok := user.Conn.Store.Contacts[jid]
+ //user.Conn.Store.ContactsLock.RUnlock()
+ //if !ok {
+ // ce.Reply("Group JID not found in contacts. Try syncing contacts with `sync` first.")
+ // return
+ //}
+ //handler.log.Debugln("Importing", jid, "for", user)
+ //portal := user.bridge.GetPortalByJID(database.GroupPortalKey(jid))
+ //if len(portal.MXID) > 0 {
+ // portal.Sync(user, contact)
+ // ce.Reply("Portal room synced.")
+ //} else {
+ // portal.Sync(user, contact)
+ // ce.Reply("Portal room created.")
+ //}
+ //_, _ = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID})
}
-const cmdPMHelp = `pm [--force] <_international phone number_> - Open a private chat with the given phone number.`
+const cmdPMHelp = `pm <_international phone number_> - Open a private chat with the given phone number.`
func (handler *CommandHandler) CommandPM(ce *CommandEvent) {
if len(ce.Args) == 0 {
- ce.Reply("**Usage:** `pm [--force] `")
+ ce.Reply("**Usage:** `pm `")
return
}
- force := ce.Args[0] == "--force"
- if force {
- ce.Args = ce.Args[1:]
- }
-
user := ce.User
number := strings.Join(ce.Args, "")
- if number[0] == '+' {
- number = number[1:]
+ resp, err := ce.User.Client.IsOnWhatsApp([]string{number})
+ if err != nil {
+ ce.Reply("Failed to check if user is on WhatsApp: %v", err)
+ return
+ } else if len(resp) == 0 {
+ ce.Reply("Didn't get a response to checking if the user is on WhatsApp")
+ return
}
- for _, char := range number {
- if char < '0' || char > '9' {
- ce.Reply("Invalid phone number.")
- return
- }
+ targetUser := resp[0]
+ if !targetUser.IsIn {
+ ce.Reply("The server said +%s is not on WhatsApp", targetUser.JID.User)
+ return
}
- jid := number + whatsapp.NewUserSuffix
- handler.log.Debugln("Importing", jid, "for", user)
-
- user.Conn.Store.ContactsLock.RLock()
- contact, ok := user.Conn.Store.Contacts[jid]
- user.Conn.Store.ContactsLock.RUnlock()
- if !ok {
- if !force {
- ce.Reply("Phone number not found in contacts. Try syncing contacts with `sync` first. " +
- "To create a portal anyway, use `pm --force `.")
- return
- }
- contact = whatsapp.Contact{JID: jid}
- }
- puppet := user.bridge.GetPuppetByJID(contact.JID)
- puppet.Sync(user, contact)
- portal := user.bridge.GetPortalByJID(database.NewPortalKey(contact.JID, user.JID))
+ handler.log.Debugln("Importing", targetUser.JID, "for", user)
+ puppet := user.bridge.GetPuppetByJID(targetUser.JID)
+ puppet.SyncContact(user, true)
+ portal := user.GetPortalByJID(puppet.JID)
if len(portal.MXID) > 0 {
- var err error
- if !user.IsRelaybot {
- err = portal.MainIntent().EnsureInvited(portal.MXID, user.MXID)
- }
- if err != nil {
- portal.log.Warnfln("Failed to invite %s to portal: %v. Creating new portal", user.MXID, err)
+ ok := portal.ensureUserInvited(user)
+ if !ok {
+ portal.log.Warnfln("ensureUserInvited(%s) returned false, creating new portal", user.MXID)
portal.MXID = ""
} else {
ce.Reply("You already have a private chat portal with that user at [%s](https://matrix.to/#/%s)", puppet.Displayname, portal.MXID)
return
}
}
- err := portal.CreateMatrixRoom(user)
+ err = portal.CreateMatrixRoom(user)
if err != nil {
ce.Reply("Failed to create portal room: %v", err)
return
diff --git a/community.go b/community.go
deleted file mode 100644
index 6ed97ee..0000000
--- a/community.go
+++ /dev/null
@@ -1,132 +0,0 @@
-// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 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 (
- "fmt"
- "net/http"
-
- "maunium.net/go/mautrix"
-)
-
-func (user *User) inviteToCommunity() {
- url := user.bridge.Bot.BuildURL("groups", user.CommunityID, "admin", "users", "invite", user.MXID)
- reqBody := map[string]interface{}{}
- _, err := user.bridge.Bot.MakeRequest(http.MethodPut, url, &reqBody, nil)
- if err != nil {
- user.log.Warnfln("Failed to invite user to personal filtering community %s: %v", user.CommunityID, err)
- }
-}
-
-func (user *User) updateCommunityProfile() {
- url := user.bridge.Bot.BuildURL("groups", user.CommunityID, "profile")
- profileReq := struct {
- Name string `json:"name"`
- AvatarURL string `json:"avatar_url"`
- ShortDescription string `json:"short_description"`
- }{"WhatsApp", user.bridge.Config.AppService.Bot.Avatar, "Your WhatsApp bridged chats"}
- _, err := user.bridge.Bot.MakeRequest(http.MethodPost, url, &profileReq, nil)
- if err != nil {
- user.log.Warnfln("Failed to update metadata of %s: %v", user.CommunityID, err)
- }
-}
-
-func (user *User) createCommunity() {
- if user.IsRelaybot || !user.bridge.Config.Bridge.EnableCommunities() {
- return
- }
-
- localpart, server, _ := user.MXID.Parse()
- community := user.bridge.Config.Bridge.FormatCommunity(localpart, server)
- user.log.Debugln("Creating personal filtering community", community)
- bot := user.bridge.Bot
- req := struct {
- Localpart string `json:"localpart"`
- }{community}
- resp := struct {
- GroupID string `json:"group_id"`
- }{}
- _, err := bot.MakeRequest(http.MethodPost, bot.BuildURL("create_group"), &req, &resp)
- if err != nil {
- if httpErr, ok := err.(mautrix.HTTPError); ok {
- if httpErr.RespError.Err != "Group already exists" {
- user.log.Warnln("Server responded with error creating personal filtering community:", err)
- return
- } else {
- user.log.Debugln("Personal filtering community", resp.GroupID, "already existed")
- user.CommunityID = fmt.Sprintf("+%s:%s", req.Localpart, user.bridge.Config.Homeserver.Domain)
- }
- } else {
- user.log.Warnln("Unknown error creating personal filtering community:", err)
- return
- }
- } else {
- user.log.Infoln("Created personal filtering community %s", resp.GroupID)
- user.CommunityID = resp.GroupID
- user.inviteToCommunity()
- user.updateCommunityProfile()
- }
-}
-
-func (user *User) addPuppetToCommunity(puppet *Puppet) bool {
- if user.IsRelaybot || len(user.CommunityID) == 0 {
- return false
- }
- bot := user.bridge.Bot
- url := bot.BuildURL("groups", user.CommunityID, "admin", "users", "invite", puppet.MXID)
- blankReqBody := map[string]interface{}{}
- _, err := bot.MakeRequest(http.MethodPut, url, &blankReqBody, nil)
- if err != nil {
- user.log.Warnfln("Failed to invite %s to %s: %v", puppet.MXID, user.CommunityID, err)
- return false
- }
- reqBody := map[string]map[string]string{
- "m.visibility": {
- "type": "private",
- },
- }
- url = bot.BuildURLWithQuery(mautrix.URLPath{"groups", user.CommunityID, "self", "accept_invite"}, map[string]string{
- "user_id": puppet.MXID.String(),
- })
- _, err = bot.MakeRequest(http.MethodPut, url, &reqBody, nil)
- if err != nil {
- user.log.Warnfln("Failed to join %s as %s: %v", user.CommunityID, puppet.MXID, err)
- return false
- }
- user.log.Debugln("Added", puppet.MXID, "to", user.CommunityID)
- return true
-}
-
-func (user *User) addPortalToCommunity(portal *Portal) bool {
- if user.IsRelaybot || len(user.CommunityID) == 0 || len(portal.MXID) == 0 {
- return false
- }
- bot := user.bridge.Bot
- url := bot.BuildURL("groups", user.CommunityID, "admin", "rooms", portal.MXID)
- reqBody := map[string]map[string]string{
- "m.visibility": {
- "type": "private",
- },
- }
- _, err := bot.MakeRequest(http.MethodPut, url, &reqBody, nil)
- if err != nil {
- user.log.Warnfln("Failed to add %s to %s: %v", portal.MXID, user.CommunityID, err)
- return false
- }
- user.log.Debugln("Added", portal.MXID, "to", user.CommunityID)
- return true
-}
diff --git a/config/bridge.go b/config/bridge.go
index e6667b0..bf1d367 100644
--- a/config/bridge.go
+++ b/config/bridge.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -17,12 +17,11 @@
package config
import (
- "bytes"
"strconv"
"strings"
"text/template"
- "github.com/Rhymen/go-whatsapp"
+ "go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
@@ -31,18 +30,8 @@ import (
type BridgeConfig struct {
UsernameTemplate string `yaml:"username_template"`
DisplaynameTemplate string `yaml:"displayname_template"`
- CommunityTemplate string `yaml:"community_template"`
- ConnectionTimeout int `yaml:"connection_timeout"`
- FetchMessageOnTimeout bool `yaml:"fetch_message_on_timeout"`
DeliveryReceipts bool `yaml:"delivery_receipts"`
- MaxConnectionAttempts int `yaml:"max_connection_attempts"`
- ConnectionRetryDelay int `yaml:"connection_retry_delay"`
- ReportConnectionRetry bool `yaml:"report_connection_retry"`
- AggressiveReconnect bool `yaml:"aggressive_reconnect"`
- ChatListWait int `yaml:"chat_list_wait"`
- PortalSyncWait int `yaml:"portal_sync_wait"`
- UserMessageBuffer int `yaml:"user_message_buffer"`
PortalMessageBuffer int `yaml:"portal_message_buffer"`
CallNotices struct {
@@ -50,15 +39,13 @@ type BridgeConfig struct {
End bool `yaml:"end"`
} `yaml:"call_notices"`
- InitialChatSync int `yaml:"initial_chat_sync_count"`
- InitialHistoryFill int `yaml:"initial_history_fill_count"`
- HistoryDisableNotifs bool `yaml:"initial_history_disable_notifications"`
- RecoverChatSync int `yaml:"recovery_chat_sync_count"`
- RecoverHistory bool `yaml:"recovery_history_backfill"`
- ChatMetaSync bool `yaml:"chat_meta_sync"`
- UserAvatarSync bool `yaml:"user_avatar_sync"`
- BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`
- SyncChatMaxAge int64 `yaml:"sync_max_chat_age"`
+ HistorySync struct {
+ CreatePortals bool `yaml:"create_portals"`
+ Backfill bool `yaml:"backfill"`
+ DoublePuppetBackfill bool `yaml:"double_puppet_backfill"`
+ } `yaml:"history_sync"`
+ UserAvatarSync bool `yaml:"user_avatar_sync"`
+ BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`
SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"`
SyncDirectChatList bool `yaml:"sync_direct_chat_list"`
@@ -66,16 +53,15 @@ type BridgeConfig struct {
DefaultBridgePresence bool `yaml:"default_bridge_presence"`
LoginSharedSecret string `yaml:"login_shared_secret"`
- InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"`
- PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"`
- BridgeNotices bool `yaml:"bridge_notices"`
- ResendBridgeInfo bool `yaml:"resend_bridge_info"`
- MuteBridging bool `yaml:"mute_bridging"`
- ArchiveTag string `yaml:"archive_tag"`
- PinnedTag string `yaml:"pinned_tag"`
- TagOnlyOnCreate bool `yaml:"tag_only_on_create"`
- MarkReadOnlyOnCreate bool `yaml:"mark_read_only_on_create"`
- EnableStatusBroadcast bool `yaml:"enable_status_broadcast"`
+ PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"`
+ BridgeNotices bool `yaml:"bridge_notices"`
+ ResendBridgeInfo bool `yaml:"resend_bridge_info"`
+ MuteBridging bool `yaml:"mute_bridging"`
+ ArchiveTag string `yaml:"archive_tag"`
+ PinnedTag string `yaml:"pinned_tag"`
+ TagOnlyOnCreate bool `yaml:"tag_only_on_create"`
+ MarkReadOnlyOnCreate bool `yaml:"mark_read_only_on_create"`
+ EnableStatusBroadcast bool `yaml:"enable_status_broadcast"`
WhatsappThumbnail bool `yaml:"whatsapp_thumbnail"`
@@ -103,44 +89,26 @@ type BridgeConfig struct {
Permissions PermissionConfig `yaml:"permissions"`
- Relaybot RelaybotConfig `yaml:"relaybot"`
+ Relay RelaybotConfig `yaml:"relay"`
usernameTemplate *template.Template `yaml:"-"`
displaynameTemplate *template.Template `yaml:"-"`
- communityTemplate *template.Template `yaml:"-"`
}
func (bc *BridgeConfig) setDefaults() {
- bc.ConnectionTimeout = 20
- bc.FetchMessageOnTimeout = false
- bc.DeliveryReceipts = false
- bc.MaxConnectionAttempts = 3
- bc.ConnectionRetryDelay = -1
- bc.ReportConnectionRetry = true
- bc.ChatListWait = 30
- bc.PortalSyncWait = 600
- bc.UserMessageBuffer = 1024
bc.PortalMessageBuffer = 128
bc.CallNotices.Start = true
bc.CallNotices.End = true
- bc.InitialChatSync = 10
- bc.InitialHistoryFill = 20
- bc.RecoverChatSync = -1
- bc.RecoverHistory = true
- bc.ChatMetaSync = true
+ bc.HistorySync.CreatePortals = true
bc.UserAvatarSync = true
bc.BridgeMatrixLeave = true
- bc.SyncChatMaxAge = 259200
bc.SyncWithCustomPuppets = true
bc.DefaultBridgePresence = true
bc.DefaultBridgeReceipts = true
- bc.LoginSharedSecret = ""
- bc.InviteOwnPuppetForBackfilling = true
- bc.PrivateChatPortalMeta = false
bc.BridgeNotices = true
bc.EnableStatusBroadcast = true
@@ -167,13 +135,6 @@ func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
- if len(bc.CommunityTemplate) > 0 {
- bc.communityTemplate, err = template.New("community").Parse(bc.CommunityTemplate)
- if err != nil {
- return err
- }
- }
-
return nil
}
@@ -181,44 +142,43 @@ type UsernameTemplateArgs struct {
UserID id.UserID
}
-func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8) {
- var buf bytes.Buffer
- if index := strings.IndexRune(contact.JID, '@'); index > 0 {
- contact.JID = "+" + contact.JID[:index]
- }
- bc.displaynameTemplate.Execute(&buf, contact)
+type legacyContactInfo struct {
+ types.ContactInfo
+ Phone string
+
+ Notify string
+ VName string
+ Name string
+ Short string
+ JID string
+}
+
+func (bc BridgeConfig) FormatDisplayname(jid types.JID, contact types.ContactInfo) (string, int8) {
+ var buf strings.Builder
+ _ = bc.displaynameTemplate.Execute(&buf, legacyContactInfo{
+ ContactInfo: contact,
+ Notify: contact.PushName,
+ VName: contact.BusinessName,
+ Name: contact.FullName,
+ Short: contact.FirstName,
+ Phone: "+" + jid.User,
+ JID: "+" + jid.User,
+ })
var quality int8
switch {
- case len(contact.Notify) > 0 || len(contact.VName) > 0:
+ case len(contact.PushName) > 0 || len(contact.BusinessName) > 0:
quality = 3
- case len(contact.Name) > 0 || len(contact.Short) > 0:
+ case len(contact.FullName) > 0 || len(contact.FirstName) > 0:
quality = 2
- case len(contact.JID) > 0:
- quality = 1
default:
- quality = 0
+ quality = 1
}
return buf.String(), quality
}
-func (bc BridgeConfig) FormatUsername(userID whatsapp.JID) string {
- var buf bytes.Buffer
- bc.usernameTemplate.Execute(&buf, userID)
- return buf.String()
-}
-
-type CommunityTemplateArgs struct {
- Localpart string
- Server string
-}
-
-func (bc BridgeConfig) EnableCommunities() bool {
- return bc.communityTemplate != nil
-}
-
-func (bc BridgeConfig) FormatCommunity(localpart, server string) string {
- var buf bytes.Buffer
- bc.communityTemplate.Execute(&buf, CommunityTemplateArgs{localpart, server})
+func (bc BridgeConfig) FormatUsername(username string) string {
+ var buf strings.Builder
+ _ = bc.usernameTemplate.Execute(&buf, username)
return buf.String()
}
@@ -227,10 +187,10 @@ type PermissionConfig map[string]PermissionLevel
type PermissionLevel int
const (
- PermissionLevelDefault PermissionLevel = 0
- PermissionLevelRelaybot PermissionLevel = 5
- PermissionLevelUser PermissionLevel = 10
- PermissionLevelAdmin PermissionLevel = 100
+ PermissionLevelDefault PermissionLevel = 0
+ PermissionLevelRelay PermissionLevel = 5
+ PermissionLevelUser PermissionLevel = 10
+ PermissionLevelAdmin PermissionLevel = 100
)
func (pc *PermissionConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
@@ -245,8 +205,8 @@ func (pc *PermissionConfig) UnmarshalYAML(unmarshal func(interface{}) error) err
}
for key, value := range rawPC {
switch strings.ToLower(value) {
- case "relaybot":
- (*pc)[key] = PermissionLevelRelaybot
+ case "relaybot", "relay":
+ (*pc)[key] = PermissionLevelRelay
case "user":
(*pc)[key] = PermissionLevelUser
case "admin":
@@ -270,8 +230,8 @@ func (pc *PermissionConfig) MarshalYAML() (interface{}, error) {
rawPC := make(map[string]string)
for key, value := range *pc {
switch value {
- case PermissionLevelRelaybot:
- rawPC[key] = "relaybot"
+ case PermissionLevelRelay:
+ rawPC[key] = "relay"
case PermissionLevelUser:
rawPC[key] = "user"
case PermissionLevelAdmin:
@@ -283,8 +243,8 @@ func (pc *PermissionConfig) MarshalYAML() (interface{}, error) {
return rawPC, nil
}
-func (pc PermissionConfig) IsRelaybotWhitelisted(userID id.UserID) bool {
- return pc.GetPermissionLevel(userID) >= PermissionLevelRelaybot
+func (pc PermissionConfig) IsRelayWhitelisted(userID id.UserID) bool {
+ return pc.GetPermissionLevel(userID) >= PermissionLevelRelay
}
func (pc PermissionConfig) IsWhitelisted(userID id.UserID) bool {
@@ -316,10 +276,8 @@ func (pc PermissionConfig) GetPermissionLevel(userID id.UserID) PermissionLevel
}
type RelaybotConfig struct {
- Enabled bool `yaml:"enabled"`
- ManagementRoom id.RoomID `yaml:"management"`
- InviteUsers []id.UserID `yaml:"invites"`
-
+ Enabled bool `yaml:"enabled"`
+ AdminOnly bool `yaml:"admin_only"`
MessageFormats map[event.MessageType]string `yaml:"message_formats"`
messageTemplates *template.Template `yaml:"-"`
}
diff --git a/crypto.go b/crypto.go
index 4e41266..402e8f4 100644
--- a/crypto.go
+++ b/crypto.go
@@ -14,6 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+//go:build cgo && !nocrypto
// +build cgo,!nocrypto
package main
@@ -100,7 +101,8 @@ func (helper *CryptoHelper) allowKeyShare(device *crypto.DeviceIdentity, info ev
return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnavailable, Reason: "Requested room is not a portal room"}
}
user := helper.bridge.GetUserByMXID(device.UserID)
- if !user.Admin && !user.IsInPortal(portal.Key) {
+ // FIXME reimplement IsInPortal
+ if !user.Admin /*&& !user.IsInPortal(portal.Key)*/ {
helper.log.Debugfln("Rejecting key request for %s from %s/%s: user is not in portal", info.SessionID, device.UserID, device.DeviceID)
return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnauthorized, Reason: "You're not in that portal"}
}
diff --git a/custompuppet.go b/custompuppet.go
index 286cad7..e7eb124 100644
--- a/custompuppet.go
+++ b/custompuppet.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -23,7 +23,7 @@ import (
"errors"
"time"
- "github.com/Rhymen/go-whatsapp"
+ "go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
@@ -160,7 +160,7 @@ func (puppet *Puppet) stopSyncing() {
}
func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
- if !puppet.customUser.IsConnected() {
+ if !puppet.customUser.IsLoggedIn() {
puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp")
return nil
}
@@ -200,14 +200,14 @@ func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
}
func (puppet *Puppet) handlePresenceEvent(event *event.Event) {
- presence := whatsapp.PresenceAvailable
+ presence := types.PresenceAvailable
if event.Content.Raw["presence"].(string) != "online" {
- presence = whatsapp.PresenceUnavailable
+ presence = types.PresenceUnavailable
puppet.customUser.log.Debugln("Marking offline")
} else {
puppet.customUser.log.Debugln("Marking online")
}
- _, err := puppet.customUser.Conn.Presence("", presence)
+ err := puppet.customUser.Client.SendPresence(presence)
if err != nil {
puppet.customUser.log.Warnln("Failed to set presence:", err)
}
@@ -222,7 +222,7 @@ func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) {
// Ignore double puppeted read receipts.
} else if message := puppet.bridge.DB.Message.GetByMXID(eventID); message != nil {
puppet.customUser.log.Debugfln("Marking %s/%s in %s/%s as read", message.JID, message.MXID, portal.Key.JID, portal.MXID)
- _, err := puppet.customUser.Conn.Read(portal.Key.JID, message.JID)
+ err := puppet.customUser.Client.MarkRead([]types.MessageID{message.JID}, time.UnixMilli(receipt.Timestamp), portal.Key.JID, message.Sender)
if err != nil {
puppet.customUser.log.Warnln("Error marking read:", err)
}
@@ -240,14 +240,14 @@ func (puppet *Puppet) handleTypingEvent(portal *Portal, evt *event.Event) {
}
if puppet.customTypingIn[evt.RoomID] != isTyping {
puppet.customTypingIn[evt.RoomID] = isTyping
- presence := whatsapp.PresenceComposing
+ presence := types.ChatPresenceComposing
if !isTyping {
puppet.customUser.log.Debugfln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID)
- presence = whatsapp.PresencePaused
+ presence = types.ChatPresencePaused
} else {
puppet.customUser.log.Debugfln("Marking typing in %s/%s", portal.Key.JID, portal.MXID)
}
- _, err := puppet.customUser.Conn.Presence(portal.Key.JID, presence)
+ err := puppet.customUser.Client.SendChatPresence(presence, portal.Key.JID)
if err != nil {
puppet.customUser.log.Warnln("Error setting typing:", err)
}
diff --git a/database/cryptostore.go b/database/cryptostore.go
index 618d150..872a9d6 100644
--- a/database/cryptostore.go
+++ b/database/cryptostore.go
@@ -14,6 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+//go:build cgo && !nocrypto
// +build cgo,!nocrypto
package database
diff --git a/database/database.go b/database/database.go
index 6d6baa3..c920521 100644
--- a/database/database.go
+++ b/database/database.go
@@ -19,14 +19,19 @@ package database
import (
"database/sql"
- _ "github.com/lib/pq"
+ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
log "maunium.net/go/maulogger/v2"
+ "go.mau.fi/whatsmeow/store/sqlstore"
"maunium.net/go/mautrix-whatsapp/database/upgrades"
)
+func init() {
+ sqlstore.PostgresArrayWrapper = pq.Array
+}
+
type Database struct {
*sql.DB
log log.Logger
diff --git a/database/message.go b/database/message.go
index 83ca81a..2ec7e95 100644
--- a/database/message.go
+++ b/database/message.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -21,11 +21,11 @@ import (
"strings"
"time"
- "github.com/Rhymen/go-whatsapp"
-
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
+
+ "go.mau.fi/whatsmeow/types"
)
type MessageQuery struct {
@@ -40,45 +40,66 @@ func (mq *MessageQuery) New() *Message {
}
}
+const (
+ getAllMessagesQuery = `
+ SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
+ WHERE chat_jid=$1 AND chat_receiver=$2
+ `
+ getMessageByJIDQuery = `
+ SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
+ WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3
+ `
+ getMessageByMXIDQuery = `
+ SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
+ WHERE mxid=$1
+ `
+ getLastMessageInChatQuery = `
+ SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
+ WHERE chat_jid=$1 AND chat_receiver=$2 AND timestamp<=$3 AND sent=true ORDER BY timestamp DESC LIMIT 1
+ `
+ getFirstMessageInChatQuery = `
+ SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
+ WHERE chat_jid=$1 AND chat_receiver=$2 AND sent=true ORDER BY timestamp ASC LIMIT 1
+ `
+)
+
func (mq *MessageQuery) GetAll(chat PortalKey) (messages []*Message) {
- rows, err := mq.db.Query("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent FROM message WHERE chat_jid=$1 AND chat_receiver=$2", chat.JID, chat.Receiver)
+ rows, err := mq.db.Query(getAllMessagesQuery, chat.JID, chat.Receiver)
if err != nil || rows == nil {
return nil
}
- defer rows.Close()
for rows.Next() {
messages = append(messages, mq.New().Scan(rows))
}
return
}
-func (mq *MessageQuery) GetByJID(chat PortalKey, jid whatsapp.MessageID) *Message {
- return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent "+
- "FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", chat.JID, chat.Receiver, jid)
+func (mq *MessageQuery) GetByJID(chat PortalKey, jid types.MessageID) *Message {
+ return mq.maybeScan(mq.db.QueryRow(getMessageByJIDQuery, chat.JID, chat.Receiver, jid))
}
func (mq *MessageQuery) GetByMXID(mxid id.EventID) *Message {
- return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent "+
- "FROM message WHERE mxid=$1", mxid)
+ return mq.maybeScan(mq.db.QueryRow(getMessageByMXIDQuery, mxid))
}
func (mq *MessageQuery) GetLastInChat(chat PortalKey) *Message {
- return mq.GetLastInChatBefore(chat, time.Now().Unix()+60)
+ return mq.GetLastInChatBefore(chat, time.Now().Add(60*time.Second))
}
-func (mq *MessageQuery) GetLastInChatBefore(chat PortalKey, maxTimestamp int64) *Message {
- msg := mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent "+
- "FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND timestamp<=$3 AND sent=true ORDER BY timestamp DESC LIMIT 1",
- chat.JID, chat.Receiver, maxTimestamp)
- if msg == nil || msg.Timestamp == 0 {
+func (mq *MessageQuery) GetLastInChatBefore(chat PortalKey, maxTimestamp time.Time) *Message {
+ msg := mq.maybeScan(mq.db.QueryRow(getLastMessageInChatQuery, chat.JID, chat.Receiver, maxTimestamp.Unix()))
+ if msg == nil || msg.Timestamp.IsZero() {
// Old db, we don't know what the last message is.
return nil
}
return msg
}
-func (mq *MessageQuery) get(query string, args ...interface{}) *Message {
- row := mq.db.QueryRow(query, args...)
+func (mq *MessageQuery) GetFirstInChat(chat PortalKey) *Message {
+ return mq.maybeScan(mq.db.QueryRow(getFirstMessageInChatQuery, chat.JID, chat.Receiver))
+}
+
+func (mq *MessageQuery) maybeScan(row *sql.Row) *Message {
if row == nil {
return nil
}
@@ -90,11 +111,13 @@ type Message struct {
log log.Logger
Chat PortalKey
- JID whatsapp.MessageID
+ JID types.MessageID
MXID id.EventID
- Sender whatsapp.JID
- Timestamp int64
+ Sender types.JID
+ Timestamp time.Time
Sent bool
+
+ DecryptionError bool
}
func (msg *Message) IsFakeMXID() bool {
@@ -102,22 +125,30 @@ func (msg *Message) IsFakeMXID() bool {
}
func (msg *Message) Scan(row Scannable) *Message {
- err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &msg.Timestamp, &msg.Sent)
+ var ts int64
+ err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &ts, &msg.Sent, &msg.DecryptionError)
if err != nil {
if err != sql.ErrNoRows {
msg.log.Errorln("Database scan failed:", err)
}
return nil
}
-
+ if ts != 0 {
+ msg.Timestamp = time.Unix(ts, 0)
+ }
return msg
}
func (msg *Message) Insert() {
+ var sender interface{} = msg.Sender
+ // Slightly hacky hack to allow inserting empty senders (used for post-backfill dummy events)
+ if msg.Sender.IsEmpty() {
+ sender = ""
+ }
_, err := msg.db.Exec(`INSERT INTO message
- (chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent)
- VALUES ($1, $2, $3, $4, $5, $6, $7)`,
- msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, msg.Sender, msg.Timestamp, msg.Sent)
+ (chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
+ msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, sender, msg.Timestamp.Unix(), msg.Sent, msg.DecryptionError)
if err != nil {
msg.log.Warnfln("Failed to insert %s@%s: %v", msg.Chat, msg.JID, err)
}
@@ -131,6 +162,15 @@ func (msg *Message) MarkSent() {
}
}
+func (msg *Message) UpdateMXID(mxid id.EventID, stillDecryptionError bool) {
+ msg.MXID = mxid
+ msg.DecryptionError = stillDecryptionError
+ _, err := msg.db.Exec("UPDATE message SET mxid=$4, decryption_error=$5 WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", msg.Chat.JID, msg.Chat.Receiver, msg.JID, mxid, stillDecryptionError)
+ if err != nil {
+ msg.log.Warnfln("Failed to update %s@%s: %v", msg.Chat, msg.JID, err)
+ }
+}
+
func (msg *Message) Delete() {
_, err := msg.db.Exec("DELETE FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", msg.Chat.JID, msg.Chat.Receiver, msg.JID)
if err != nil {
diff --git a/database/portal.go b/database/portal.go
index 3f26fac..35a494e 100644
--- a/database/portal.go
+++ b/database/portal.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -18,42 +18,40 @@ package database
import (
"database/sql"
- "strings"
log "maunium.net/go/maulogger/v2"
- "github.com/Rhymen/go-whatsapp"
-
"maunium.net/go/mautrix/id"
+
+ "go.mau.fi/whatsmeow/types"
)
type PortalKey struct {
- JID whatsapp.JID
- Receiver whatsapp.JID
+ JID types.JID
+ Receiver types.JID
}
-func GroupPortalKey(jid whatsapp.JID) PortalKey {
- return PortalKey{
- JID: jid,
- Receiver: jid,
- }
+func GroupPortalKey(jid types.JID) PortalKey {
+ return NewPortalKey(jid, jid)
}
-func NewPortalKey(jid, receiver whatsapp.JID) PortalKey {
- if strings.HasSuffix(jid, whatsapp.GroupSuffix) {
+func NewPortalKey(jid, receiver types.JID) PortalKey {
+ if jid.Server == types.GroupServer {
receiver = jid
+ } else if jid.Server == types.LegacyUserServer {
+ jid.Server = types.DefaultUserServer
}
return PortalKey{
- JID: jid,
- Receiver: receiver,
+ JID: jid.ToNonAD(),
+ Receiver: receiver.ToNonAD(),
}
}
func (key PortalKey) String() string {
if key.Receiver == key.JID {
- return key.JID
+ return key.JID.String()
}
- return key.JID + "-" + key.Receiver
+ return key.JID.String() + "-" + key.Receiver.String()
}
type PortalQuery struct {
@@ -80,12 +78,12 @@ func (pq *PortalQuery) GetByMXID(mxid id.RoomID) *Portal {
return pq.get("SELECT * FROM portal WHERE mxid=$1", mxid)
}
-func (pq *PortalQuery) GetAllByJID(jid whatsapp.JID) []*Portal {
- return pq.getAll("SELECT * FROM portal WHERE jid=$1", jid)
+func (pq *PortalQuery) GetAllByJID(jid types.JID) []*Portal {
+ return pq.getAll("SELECT * FROM portal WHERE jid=$1", jid.ToNonAD())
}
-func (pq *PortalQuery) FindPrivateChats(receiver whatsapp.JID) []*Portal {
- return pq.getAll("SELECT * FROM portal WHERE receiver=$1 AND jid LIKE '%@s.whatsapp.net'", receiver)
+func (pq *PortalQuery) FindPrivateChats(receiver types.JID) []*Portal {
+ return pq.getAll("SELECT * FROM portal WHERE receiver=$1 AND jid LIKE '%@s.whatsapp.net'", receiver.ToNonAD())
}
func (pq *PortalQuery) getAll(query string, args ...interface{}) (portals []*Portal) {
@@ -120,11 +118,16 @@ type Portal struct {
Avatar string
AvatarURL id.ContentURI
Encrypted bool
+
+ FirstEventID id.EventID
+ NextBatchID id.BatchID
+
+ RelayUserID id.UserID
}
func (portal *Portal) Scan(row Scannable) *Portal {
- var mxid, avatarURL sql.NullString
- err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL, &portal.Encrypted)
+ var mxid, avatarURL, firstEventID, nextBatchID, relayUserID sql.NullString
+ err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL, &portal.Encrypted, &firstEventID, &nextBatchID, &relayUserID)
if err != nil {
if err != sql.ErrNoRows {
portal.log.Errorln("Database scan failed:", err)
@@ -133,6 +136,9 @@ func (portal *Portal) Scan(row Scannable) *Portal {
}
portal.MXID = id.RoomID(mxid.String)
portal.AvatarURL, _ = id.ParseContentURI(avatarURL.String)
+ portal.FirstEventID = id.EventID(firstEventID.String)
+ portal.NextBatchID = id.BatchID(nextBatchID.String)
+ portal.RelayUserID = id.UserID(relayUserID.String)
return portal
}
@@ -143,21 +149,24 @@ func (portal *Portal) mxidPtr() *id.RoomID {
return nil
}
+func (portal *Portal) relayUserPtr() *id.UserID {
+ if len(portal.RelayUserID) > 0 {
+ return &portal.RelayUserID
+ }
+ return nil
+}
+
func (portal *Portal) Insert() {
- _, err := portal.db.Exec("INSERT INTO portal (jid, receiver, mxid, name, topic, avatar, avatar_url, encrypted) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
- portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted)
+ _, err := portal.db.Exec("INSERT INTO portal (jid, receiver, mxid, name, topic, avatar, avatar_url, encrypted, first_event_id, next_batch_id, relay_user_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
+ portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr())
if err != nil {
portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err)
}
}
func (portal *Portal) Update() {
- var mxid *id.RoomID
- if len(portal.MXID) > 0 {
- mxid = &portal.MXID
- }
- _, err := portal.db.Exec("UPDATE portal SET mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5, encrypted=$6 WHERE jid=$7 AND receiver=$8",
- mxid, portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.Key.JID, portal.Key.Receiver)
+ _, err := portal.db.Exec("UPDATE portal SET mxid=$3, name=$4, topic=$5, avatar=$6, avatar_url=$7, encrypted=$8, first_event_id=$9, next_batch_id=$10, relay_user_id=$11 WHERE jid=$1 AND receiver=$2",
+ portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr())
if err != nil {
portal.log.Warnfln("Failed to update %s: %v", portal.Key, err)
}
@@ -169,26 +178,3 @@ func (portal *Portal) Delete() {
portal.log.Warnfln("Failed to delete %s: %v", portal.Key, err)
}
}
-
-func (portal *Portal) GetUserIDs() []id.UserID {
- rows, err := portal.db.Query(`SELECT "user".mxid FROM "user", user_portal
- WHERE "user".jid=user_portal.user_jid
- AND user_portal.portal_jid=$1
- AND user_portal.portal_receiver=$2`,
- portal.Key.JID, portal.Key.Receiver)
- if err != nil {
- portal.log.Debugln("Failed to get portal user ids:", err)
- return nil
- }
- var userIDs []id.UserID
- for rows.Next() {
- var userID id.UserID
- err = rows.Scan(&userID)
- if err != nil {
- portal.log.Warnln("Failed to scan row:", err)
- continue
- }
- userIDs = append(userIDs, userID)
- }
- return userIDs
-}
diff --git a/database/puppet.go b/database/puppet.go
index b9ba2fc..186ba67 100644
--- a/database/puppet.go
+++ b/database/puppet.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -20,10 +20,9 @@ import (
"database/sql"
log "maunium.net/go/maulogger/v2"
-
- "github.com/Rhymen/go-whatsapp"
-
"maunium.net/go/mautrix/id"
+
+ "go.mau.fi/whatsmeow/types"
)
type PuppetQuery struct {
@@ -42,7 +41,7 @@ func (pq *PuppetQuery) New() *Puppet {
}
func (pq *PuppetQuery) GetAll() (puppets []*Puppet) {
- rows, err := pq.db.Query("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet")
+ rows, err := pq.db.Query("SELECT username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet")
if err != nil || rows == nil {
return nil
}
@@ -53,8 +52,8 @@ func (pq *PuppetQuery) GetAll() (puppets []*Puppet) {
return
}
-func (pq *PuppetQuery) Get(jid whatsapp.JID) *Puppet {
- row := pq.db.QueryRow("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE jid=$1", jid)
+func (pq *PuppetQuery) Get(jid types.JID) *Puppet {
+ row := pq.db.QueryRow("SELECT username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE username=$1", jid.User)
if row == nil {
return nil
}
@@ -62,7 +61,7 @@ func (pq *PuppetQuery) Get(jid whatsapp.JID) *Puppet {
}
func (pq *PuppetQuery) GetByCustomMXID(mxid id.UserID) *Puppet {
- row := pq.db.QueryRow("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE custom_mxid=$1", mxid)
+ row := pq.db.QueryRow("SELECT username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE custom_mxid=$1", mxid)
if row == nil {
return nil
}
@@ -70,7 +69,7 @@ func (pq *PuppetQuery) GetByCustomMXID(mxid id.UserID) *Puppet {
}
func (pq *PuppetQuery) GetAllWithCustomMXID() (puppets []*Puppet) {
- rows, err := pq.db.Query("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE custom_mxid<>''")
+ rows, err := pq.db.Query("SELECT username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE custom_mxid<>''")
if err != nil || rows == nil {
return nil
}
@@ -85,7 +84,7 @@ type Puppet struct {
db *Database
log log.Logger
- JID whatsapp.JID
+ JID types.JID
Avatar string
AvatarURL id.ContentURI
Displayname string
@@ -102,13 +101,15 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet {
var displayname, avatar, avatarURL, customMXID, accessToken, nextBatch sql.NullString
var quality sql.NullInt64
var enablePresence, enableReceipts sql.NullBool
- err := row.Scan(&puppet.JID, &avatar, &avatarURL, &displayname, &quality, &customMXID, &accessToken, &nextBatch, &enablePresence, &enableReceipts)
+ var username string
+ err := row.Scan(&username, &avatar, &avatarURL, &displayname, &quality, &customMXID, &accessToken, &nextBatch, &enablePresence, &enableReceipts)
if err != nil {
if err != sql.ErrNoRows {
puppet.log.Errorln("Database scan failed:", err)
}
return nil
}
+ puppet.JID = types.NewJID(username, types.DefaultUserServer)
puppet.Displayname = displayname.String
puppet.Avatar = avatar.String
puppet.AvatarURL, _ = id.ParseContentURI(avatarURL.String)
@@ -122,16 +123,20 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet {
}
func (puppet *Puppet) Insert() {
- _, err := puppet.db.Exec("INSERT INTO puppet (jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
- puppet.JID, puppet.Avatar, puppet.AvatarURL.String(), puppet.Displayname, puppet.NameQuality, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.EnablePresence, puppet.EnableReceipts)
+ if puppet.JID.Server != types.DefaultUserServer {
+ puppet.log.Warnfln("Not inserting %s: not a user", puppet.JID)
+ return
+ }
+ _, err := puppet.db.Exec("INSERT INTO puppet (username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
+ puppet.JID.User, puppet.Avatar, puppet.AvatarURL.String(), puppet.Displayname, puppet.NameQuality, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.EnablePresence, puppet.EnableReceipts)
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, avatar_url=$4, custom_mxid=$5, access_token=$6, next_batch=$7, enable_presence=$8, enable_receipts=$9 WHERE jid=$10",
- puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.AvatarURL.String(), puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.EnablePresence, puppet.EnableReceipts, puppet.JID)
+ _, err := puppet.db.Exec("UPDATE puppet SET displayname=$1, name_quality=$2, avatar=$3, avatar_url=$4, custom_mxid=$5, access_token=$6, next_batch=$7, enable_presence=$8, enable_receipts=$9 WHERE username=$10",
+ puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.AvatarURL.String(), puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.EnablePresence, puppet.EnableReceipts, puppet.JID.User)
if err != nil {
puppet.log.Warnfln("Failed to update %s->%s: %v", puppet.JID, err)
}
diff --git a/database/upgrades/2021-10-21-add-whatsmeow-store.go b/database/upgrades/2021-10-21-add-whatsmeow-store.go
new file mode 100644
index 0000000..41d7a8e
--- /dev/null
+++ b/database/upgrades/2021-10-21-add-whatsmeow-store.go
@@ -0,0 +1,13 @@
+package upgrades
+
+import (
+ "database/sql"
+
+ "go.mau.fi/whatsmeow/store/sqlstore"
+)
+
+func init() {
+ upgrades[24] = upgrade{"Add whatsmeow state store", func(tx *sql.Tx, ctx context) error {
+ return sqlstore.Upgrades[0](tx, sqlstore.NewWithDB(ctx.db, ctx.dialect.String(), nil))
+ }}
+}
diff --git a/database/upgrades/2021-10-21-multidevice-updates.go b/database/upgrades/2021-10-21-multidevice-updates.go
new file mode 100644
index 0000000..f9f1308
--- /dev/null
+++ b/database/upgrades/2021-10-21-multidevice-updates.go
@@ -0,0 +1,93 @@
+package upgrades
+
+import (
+ "database/sql"
+)
+
+func init() {
+ upgrades[25] = upgrade{"Update things for multidevice", func(tx *sql.Tx, ctx context) error {
+ // This is probably not necessary
+ _, err := tx.Exec("DROP TABLE user_portal")
+ if err != nil {
+ return err
+ }
+
+ // Remove invalid puppet rows
+ _, err = tx.Exec("DELETE FROM puppet WHERE jid LIKE '%@g.us' OR jid LIKE '%@broadcast'")
+ if err != nil {
+ return err
+ }
+ // Remove the suffix from puppets since they'll all have the same suffix
+ _, err = tx.Exec("UPDATE puppet SET jid=REPLACE(jid, '@s.whatsapp.net', '')")
+ if err != nil {
+ return err
+ }
+ // Rename column to correctly represent the new content
+ _, err = tx.Exec("ALTER TABLE puppet RENAME COLUMN jid TO username")
+ if err != nil {
+ return err
+ }
+
+ if ctx.dialect == SQLite {
+ // Message content was removed from the main message table earlier, but the backup table still exists for SQLite
+ _, err = tx.Exec("DROP TABLE IF EXISTS old_message")
+
+ _, err = tx.Exec(`ALTER TABLE "user" RENAME TO old_user`)
+ if err != nil {
+ return err
+ }
+ _, err = tx.Exec(`CREATE TABLE "user" (
+ mxid TEXT PRIMARY KEY,
+ username TEXT UNIQUE,
+ agent SMALLINT,
+ device SMALLINT,
+ management_room TEXT
+ )`)
+ if err != nil {
+ return err
+ }
+
+ // No need to copy auth data, users need to relogin anyway
+ _, err = tx.Exec(`INSERT INTO "user" (mxid, management_room, last_connection) SELECT mxid, management_room, last_connection FROM old_user`)
+ if err != nil {
+ return err
+ }
+
+ _, err = tx.Exec("DROP TABLE old_user")
+ if err != nil {
+ return err
+ }
+ } else {
+ // The jid column never actually contained the full JID, so let's rename it.
+ _, err = tx.Exec(`ALTER TABLE "user" RENAME COLUMN jid TO username`)
+ if err != nil {
+ return err
+ }
+
+ // The auth data is now in the whatsmeow_device table.
+ for _, column := range []string{"last_connection", "client_id", "client_token", "server_token", "enc_key", "mac_key"} {
+ _, err = tx.Exec(`ALTER TABLE "user" DROP COLUMN ` + column)
+ if err != nil {
+ return err
+ }
+ }
+
+ // The whatsmeow_device table is keyed by the full JID, so we need to store the other parts of the JID here too.
+ _, err = tx.Exec(`ALTER TABLE "user" ADD COLUMN agent SMALLINT`)
+ if err != nil {
+ return err
+ }
+ _, err = tx.Exec(`ALTER TABLE "user" ADD COLUMN device SMALLINT`)
+ if err != nil {
+ return err
+ }
+
+ // Clear all usernames, the users need to relogin anyway.
+ _, err = tx.Exec(`UPDATE "user" SET username=null`)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ }}
+}
diff --git a/database/upgrades/2021-10-26-portal-origin-event-id.go b/database/upgrades/2021-10-26-portal-origin-event-id.go
new file mode 100644
index 0000000..37b8908
--- /dev/null
+++ b/database/upgrades/2021-10-26-portal-origin-event-id.go
@@ -0,0 +1,19 @@
+package upgrades
+
+import (
+ "database/sql"
+)
+
+func init() {
+ upgrades[26] = upgrade{"Add columns to store infinite backfill pointers for portals", func(tx *sql.Tx, ctx context) error {
+ _, err := tx.Exec(`ALTER TABLE portal ADD COLUMN first_event_id TEXT NOT NULL DEFAULT ''`)
+ if err != nil {
+ return err
+ }
+ _, err = tx.Exec(`ALTER TABLE portal ADD COLUMN next_batch_id TEXT NOT NULL DEFAULT ''`)
+ if err != nil {
+ return err
+ }
+ return nil
+ }}
+}
diff --git a/database/upgrades/2021-10-27-message-decryption-errors.go b/database/upgrades/2021-10-27-message-decryption-errors.go
new file mode 100644
index 0000000..288709e
--- /dev/null
+++ b/database/upgrades/2021-10-27-message-decryption-errors.go
@@ -0,0 +1,12 @@
+package upgrades
+
+import (
+ "database/sql"
+)
+
+func init() {
+ upgrades[27] = upgrade{"Add marker for WhatsApp decryption errors in message table", func(tx *sql.Tx, ctx context) error {
+ _, err := tx.Exec(`ALTER TABLE message ADD COLUMN decryption_error BOOLEAN NOT NULL DEFAULT false`)
+ return err
+ }}
+}
diff --git a/database/upgrades/2021-10-28-portal-relay-user.go b/database/upgrades/2021-10-28-portal-relay-user.go
new file mode 100644
index 0000000..81beedc
--- /dev/null
+++ b/database/upgrades/2021-10-28-portal-relay-user.go
@@ -0,0 +1,12 @@
+package upgrades
+
+import (
+ "database/sql"
+)
+
+func init() {
+ upgrades[28] = upgrade{"Add relay user field to portal table", func(tx *sql.Tx, ctx context) error {
+ _, err := tx.Exec(`ALTER TABLE portal ADD COLUMN relay_user_id TEXT`)
+ return err
+ }}
+}
diff --git a/database/upgrades/upgrades.go b/database/upgrades/upgrades.go
index 9bcc1c2..c0d9260 100644
--- a/database/upgrades/upgrades.go
+++ b/database/upgrades/upgrades.go
@@ -39,7 +39,7 @@ type upgrade struct {
fn upgradeFunc
}
-const NumberOfUpgrades = 24
+const NumberOfUpgrades = 29
var upgrades [NumberOfUpgrades]upgrade
diff --git a/database/user.go b/database/user.go
index 1b0d9ce..a5d7b5e 100644
--- a/database/user.go
+++ b/database/user.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -18,15 +18,11 @@ package database
import (
"database/sql"
- "fmt"
- "strings"
- "time"
-
- "github.com/Rhymen/go-whatsapp"
log "maunium.net/go/maulogger/v2"
-
"maunium.net/go/mautrix/id"
+
+ "go.mau.fi/whatsmeow/types"
)
type UserQuery struct {
@@ -42,7 +38,7 @@ func (uq *UserQuery) New() *User {
}
func (uq *UserQuery) GetAll() (users []*User) {
- rows, err := uq.db.Query(`SELECT mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key FROM "user"`)
+ rows, err := uq.db.Query(`SELECT mxid, username, agent, device, management_room FROM "user"`)
if err != nil || rows == nil {
return nil
}
@@ -54,15 +50,15 @@ func (uq *UserQuery) GetAll() (users []*User) {
}
func (uq *UserQuery) GetByMXID(userID id.UserID) *User {
- row := uq.db.QueryRow(`SELECT mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key FROM "user" WHERE mxid=$1`, userID)
+ row := uq.db.QueryRow(`SELECT mxid, username, agent, device, management_room FROM "user" WHERE mxid=$1`, userID)
if row == nil {
return nil
}
return uq.New().Scan(row)
}
-func (uq *UserQuery) GetByJID(userID whatsapp.JID) *User {
- row := uq.db.QueryRow(`SELECT mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key FROM "user" WHERE jid=$1`, stripSuffix(userID))
+func (uq *UserQuery) GetByUsername(username string) *User {
+ row := uq.db.QueryRow(`SELECT mxid, username, agent, device, management_room FROM "user" WHERE username=$1`, username)
if row == nil {
return nil
}
@@ -74,185 +70,151 @@ type User struct {
log log.Logger
MXID id.UserID
- JID whatsapp.JID
+ JID types.JID
ManagementRoom id.RoomID
- Session *whatsapp.Session
- LastConnection int64
}
func (user *User) Scan(row Scannable) *User {
- var jid, clientID, clientToken, serverToken sql.NullString
- var encKey, macKey []byte
- err := row.Scan(&user.MXID, &jid, &user.ManagementRoom, &user.LastConnection, &clientID, &clientToken, &serverToken, &encKey, &macKey)
+ var username sql.NullString
+ var device, agent sql.NullByte
+ err := row.Scan(&user.MXID, &username, &agent, &device, &user.ManagementRoom)
if err != nil {
if err != sql.ErrNoRows {
user.log.Errorln("Database scan failed:", err)
}
return nil
}
- if len(jid.String) > 0 && len(clientID.String) > 0 {
- user.JID = jid.String + whatsapp.NewUserSuffix
- user.Session = &whatsapp.Session{
- ClientID: clientID.String,
- ClientToken: clientToken.String,
- ServerToken: serverToken.String,
- EncKey: encKey,
- MacKey: macKey,
- Wid: jid.String + whatsapp.OldUserSuffix,
- }
- } else {
- user.Session = nil
+ if len(username.String) > 0 {
+ user.JID = types.NewADJID(username.String, agent.Byte, device.Byte)
}
return user
}
-func stripSuffix(jid whatsapp.JID) string {
- if len(jid) == 0 {
- return jid
- }
-
- index := strings.IndexRune(jid, '@')
- if index < 0 {
- return jid
- }
-
- return jid[:index]
-}
-
-func (user *User) jidPtr() *string {
- if len(user.JID) > 0 {
- str := stripSuffix(user.JID)
- return &str
+func (user *User) usernamePtr() *string {
+ if !user.JID.IsEmpty() {
+ return &user.JID.User
}
return nil
}
-func (user *User) sessionUnptr() (sess whatsapp.Session) {
- if user.Session != nil {
- sess = *user.Session
+func (user *User) agentPtr() *uint8 {
+ if !user.JID.IsEmpty() {
+ return &user.JID.Agent
}
- return
+ return nil
+}
+
+func (user *User) devicePtr() *uint8 {
+ if !user.JID.IsEmpty() {
+ return &user.JID.Device
+ }
+ return nil
}
func (user *User) Insert() {
- sess := user.sessionUnptr()
- _, err := user.db.Exec(`INSERT INTO "user" (mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
- user.MXID, user.jidPtr(),
- user.ManagementRoom, user.LastConnection,
- sess.ClientID, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey)
+ _, err := user.db.Exec(`INSERT INTO "user" (mxid, username, agent, device, management_room) VALUES ($1, $2, $3, $4, $5)`,
+ user.MXID, user.usernamePtr(), user.agentPtr(), user.devicePtr(), user.ManagementRoom)
if err != nil {
user.log.Warnfln("Failed to insert %s: %v", user.MXID, err)
}
}
-func (user *User) UpdateLastConnection() {
- user.LastConnection = time.Now().Unix()
- _, err := user.db.Exec(`UPDATE "user" SET last_connection=$1 WHERE mxid=$2`,
- user.LastConnection, user.MXID)
- if err != nil {
- user.log.Warnfln("Failed to update last connection ts: %v", err)
- }
-}
-
func (user *User) Update() {
- sess := user.sessionUnptr()
- _, err := user.db.Exec(`UPDATE "user" SET jid=$1, management_room=$2, last_connection=$3, client_id=$4, client_token=$5, server_token=$6, enc_key=$7, mac_key=$8 WHERE mxid=$9`,
- user.jidPtr(), user.ManagementRoom, user.LastConnection,
- sess.ClientID, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey,
- user.MXID)
+ _, err := user.db.Exec(`UPDATE "user" SET username=$1, agent=$2, device=$3, management_room=$4 WHERE mxid=$5`,
+ user.usernamePtr(), user.agentPtr(), user.devicePtr(), user.ManagementRoom, user.MXID)
if err != nil {
user.log.Warnfln("Failed to update %s: %v", user.MXID, err)
}
}
-type PortalKeyWithMeta struct {
- PortalKey
- InCommunity bool
-}
-
-func (user *User) SetPortalKeys(newKeys []PortalKeyWithMeta) error {
- tx, err := user.db.Begin()
- if err != nil {
- return err
- }
- _, err = tx.Exec("DELETE FROM user_portal WHERE user_jid=$1", user.jidPtr())
- if err != nil {
- _ = tx.Rollback()
- return err
- }
- valueStrings := make([]string, len(newKeys))
- values := make([]interface{}, len(newKeys)*4)
- for i, key := range newKeys {
- pos := i * 4
- valueStrings[i] = fmt.Sprintf("($%d, $%d, $%d, $%d)", pos+1, pos+2, pos+3, pos+4)
- values[pos] = user.jidPtr()
- values[pos+1] = key.JID
- values[pos+2] = key.Receiver
- values[pos+3] = key.InCommunity
- }
- query := fmt.Sprintf("INSERT INTO user_portal (user_jid, portal_jid, portal_receiver, in_community) VALUES %s",
- strings.Join(valueStrings, ", "))
- _, err = tx.Exec(query, values...)
- if err != nil {
- _ = tx.Rollback()
- return err
- }
- return tx.Commit()
-}
-
-func (user *User) IsInPortal(key PortalKey) bool {
- row := user.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM user_portal WHERE user_jid=$1 AND portal_jid=$2 AND portal_receiver=$3)`, user.jidPtr(), &key.JID, &key.Receiver)
- var exists bool
- _ = row.Scan(&exists)
- return exists
-}
-
-func (user *User) GetPortalKeys() []PortalKey {
- rows, err := user.db.Query(`SELECT portal_jid, portal_receiver FROM user_portal WHERE user_jid=$1`, user.jidPtr())
- if err != nil {
- user.log.Warnln("Failed to get user portal keys:", err)
- return nil
- }
- var keys []PortalKey
- for rows.Next() {
- var key PortalKey
- err = rows.Scan(&key.JID, &key.Receiver)
- if err != nil {
- user.log.Warnln("Failed to scan row:", err)
- continue
- }
- keys = append(keys, key)
- }
- return keys
-}
-
-func (user *User) GetInCommunityMap() map[PortalKey]bool {
- rows, err := user.db.Query(`SELECT portal_jid, portal_receiver, in_community FROM user_portal WHERE user_jid=$1`, user.jidPtr())
- if err != nil {
- user.log.Warnln("Failed to get user portal keys:", err)
- return nil
- }
- keys := make(map[PortalKey]bool)
- for rows.Next() {
- var key PortalKey
- var inCommunity bool
- err = rows.Scan(&key.JID, &key.Receiver, &inCommunity)
- if err != nil {
- user.log.Warnln("Failed to scan row:", err)
- continue
- }
- keys[key] = inCommunity
- }
- return keys
-}
-
-func (user *User) CreateUserPortal(newKey PortalKeyWithMeta) {
- user.log.Debugfln("Creating new portal %s for %s", newKey.PortalKey.JID, newKey.PortalKey.Receiver)
- _, err := user.db.Exec(`INSERT INTO user_portal (user_jid, portal_jid, portal_receiver, in_community) VALUES ($1, $2, $3, $4)`,
- user.jidPtr(),
- newKey.PortalKey.JID, newKey.PortalKey.Receiver,
- newKey.InCommunity)
- if err != nil {
- user.log.Warnfln("Failed to insert %s: %v", user.MXID, err)
- }
-}
+//type PortalKeyWithMeta struct {
+// PortalKey
+// InCommunity bool
+//}
+//
+//func (user *User) SetPortalKeys(newKeys []PortalKeyWithMeta) error {
+// tx, err := user.db.Begin()
+// if err != nil {
+// return err
+// }
+// _, err = tx.Exec("DELETE FROM user_portal WHERE user_jid=$1", user.jidPtr())
+// if err != nil {
+// _ = tx.Rollback()
+// return err
+// }
+// valueStrings := make([]string, len(newKeys))
+// values := make([]interface{}, len(newKeys)*4)
+// for i, key := range newKeys {
+// pos := i * 4
+// valueStrings[i] = fmt.Sprintf("($%d, $%d, $%d, $%d)", pos+1, pos+2, pos+3, pos+4)
+// values[pos] = user.jidPtr()
+// values[pos+1] = key.JID
+// values[pos+2] = key.Receiver
+// values[pos+3] = key.InCommunity
+// }
+// query := fmt.Sprintf("INSERT INTO user_portal (user_jid, portal_jid, portal_receiver, in_community) VALUES %s",
+// strings.Join(valueStrings, ", "))
+// _, err = tx.Exec(query, values...)
+// if err != nil {
+// _ = tx.Rollback()
+// return err
+// }
+// return tx.Commit()
+//}
+//
+//func (user *User) IsInPortal(key PortalKey) bool {
+// row := user.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM user_portal WHERE user_jid=$1 AND portal_jid=$2 AND portal_receiver=$3)`, user.jidPtr(), &key.JID, &key.Receiver)
+// var exists bool
+// _ = row.Scan(&exists)
+// return exists
+//}
+//
+//func (user *User) GetPortalKeys() []PortalKey {
+// rows, err := user.db.Query(`SELECT portal_jid, portal_receiver FROM user_portal WHERE user_jid=$1`, user.jidPtr())
+// if err != nil {
+// user.log.Warnln("Failed to get user portal keys:", err)
+// return nil
+// }
+// var keys []PortalKey
+// for rows.Next() {
+// var key PortalKey
+// err = rows.Scan(&key.JID, &key.Receiver)
+// if err != nil {
+// user.log.Warnln("Failed to scan row:", err)
+// continue
+// }
+// keys = append(keys, key)
+// }
+// return keys
+//}
+//
+//func (user *User) GetInCommunityMap() map[PortalKey]bool {
+// rows, err := user.db.Query(`SELECT portal_jid, portal_receiver, in_community FROM user_portal WHERE user_jid=$1`, user.jidPtr())
+// if err != nil {
+// user.log.Warnln("Failed to get user portal keys:", err)
+// return nil
+// }
+// keys := make(map[PortalKey]bool)
+// for rows.Next() {
+// var key PortalKey
+// var inCommunity bool
+// err = rows.Scan(&key.JID, &key.Receiver, &inCommunity)
+// if err != nil {
+// user.log.Warnln("Failed to scan row:", err)
+// continue
+// }
+// keys[key] = inCommunity
+// }
+// return keys
+//}
+//
+//func (user *User) CreateUserPortal(newKey PortalKeyWithMeta) {
+// user.log.Debugfln("Creating new portal %s for %s", newKey.PortalKey.JID, newKey.PortalKey.Receiver)
+// _, err := user.db.Exec(`INSERT INTO user_portal (user_jid, portal_jid, portal_receiver, in_community) VALUES ($1, $2, $3, $4)`,
+// user.jidPtr(),
+// newKey.PortalKey.JID, newKey.PortalKey.Receiver,
+// newKey.InCommunity)
+// if err != nil {
+// user.log.Warnfln("Failed to insert %s: %v", user.MXID, err)
+// }
+//}
diff --git a/example-config.yaml b/example-config.yaml
index dd378f9..e2d84f4 100644
--- a/example-config.yaml
+++ b/example-config.yaml
@@ -27,6 +27,7 @@ appservice:
# The database URI.
# SQLite: File name is enough. https://github.com/mattn/go-sqlite3#connection-string
# Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable
+ # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql
uri: mautrix-whatsapp.db
# Maximum number of connections. Mostly relevant for Postgres.
max_open_conns: 20
@@ -63,9 +64,10 @@ metrics:
whatsapp:
# Device name that's shown in the "WhatsApp Web" section in the mobile app.
os_name: Mautrix-WhatsApp bridge
- # Browser name that determines the logo shown in the mobile app. If the name is unrecognized, a generic icon is shown.
- # Use the name of an actual browser (Chrome, Firefox, Safari, IE, Edge, Opera) if you want a specific icon.
- browser_name: mx-wa
+ # Browser name that determines the logo shown in the mobile app.
+ # Must be "unknown" for a generic icon or a valid browser name if you want a specific icon.
+ # List of valid browser names: https://github.com/tulir/whatsmeow/blob/2a72655ef600a7fd7a2e98d53ec6da029759c4b8/binary/proto/def.proto#L1582-L1594
+ browser_name: unknown
# Bridge config
bridge:
@@ -73,49 +75,19 @@ bridge:
# {{.}} is replaced with the phone number of the WhatsApp user.
username_template: whatsapp_{{.}}
# Displayname template for WhatsApp users.
- # {{.Notify}} - nickname set by the WhatsApp user
- # {{.VName}} - validated WhatsApp business name
- # {{.JID}} - phone number (international format)
+ # {{.PushName}} - nickname set by the WhatsApp user
+ # {{.BusinessName}} - validated WhatsApp business name
+ # {{.Phone}} - phone number (international format)
# The following variables are also available, but will cause problems on multi-user instances:
- # {{.Name}} - display name from contact list
- # {{.Short}} - short display name from contact list
- displayname_template: "{{if .Notify}}{{.Notify}}{{else if .VName}}{{.VName}}{{else}}{{.JID}}{{end}} (WA)"
- # Localpart template for per-user room grouping community IDs.
- # On startup, the bridge will try to create these communities, add all of the specific user's
- # portals to the community, and invite the Matrix user to it.
- # (Note that, by default, non-admins might not have your homeserver's permission to create
- # communities.)
- # {{.Localpart}} is the MXID localpart and {{.Server}} is the MXID server part of the user.
- # whatsapp_{{.Localpart}}={{.Server}} is a good value that should work for any user.
- community_template: null
+ # {{.FullName}} - full name from contact list
+ # {{.FirstName}} - first name from contact list
+ displayname_template: "{{if .PushName}}{{.PushName}}{{else if .BusinessName}}{{.BusinessName}}{{else}}{{.JID}}{{end}} (WA)"
- # WhatsApp connection timeout in seconds.
- connection_timeout: 20
- # If WhatsApp doesn't respond within connection_timeout, should the bridge try to fetch the message
- # to see if it was actually bridged? Use this if you have problems with sends timing out but actually
- # succeeding.
- fetch_message_on_timeout: false
# Whether or not the bridge should send a read receipt from the bridge bot when a message has been
# sent to WhatsApp. If fetch_message_on_timeout is enabled, a successful post-timeout fetch will
# trigger a read receipt too.
delivery_receipts: false
- # Maximum number of times to retry connecting on connection error.
- max_connection_attempts: 3
- # Number of seconds to wait between connection attempts.
- # Negative numbers are exponential backoff: -connection_retry_delay + 1 + 2^attempts
- connection_retry_delay: -1
- # Whether or not the bridge should send a notice to the user's management room when it retries connecting.
- # If false, it will only report when it stops retrying.
- report_connection_retry: true
- # Whether or not the bridge should reconnect even if WhatsApp says another web client connected.
- aggressive_reconnect: false
- # Maximum number of seconds to wait for chats to be sent at startup.
- # If this is too low and you have lots of chats, it could cause backfilling to fail.
- chat_list_wait: 30
- # Maximum number of seconds to wait to sync portals before force unlocking message processing.
- # If this is too low and you have lots of chats, it could cause backfilling to fail.
- portal_sync_wait: 600
- user_message_buffer: 1024
+
portal_message_buffer: 128
# Whether or not to send call start/end notices to Matrix.
@@ -123,32 +95,20 @@ bridge:
start: true
end: true
- # Number of chats to sync for new users.
- initial_chat_sync_count: 10
- # Number of old messages to fill when creating new portal rooms.
- initial_history_fill_count: 20
- # Whether or not notifications should be turned off while filling initial history.
- # Only applicable when using double puppeting.
- initial_history_disable_notifications: false
- # Maximum number of chats to sync when recovering from downtime.
- # Set to -1 to sync all new chats during downtime.
- recovery_chat_sync_limit: -1
- # Whether or not to sync history when recovering from downtime.
- recovery_history_backfill: true
- # Whether or not portal info should be fetched from the server when syncing,
- # instead of relying on finding any changes in the message history.
- # If you get 599 errors often, you should try disabling this.
- chat_meta_sync: true
+ history_sync:
+ # Whether to create portals from history sync payloads from WhatsApp.
+ create_portals: true
+ # Whether to enable backfilling history sync payloads from WhatsApp using batch sending
+ # This requires a server with MSC2716 support, which is currently an experimental feature in synapse.
+ # It can be enabled by setting experimental_features -> enable_msc2716 to true in homeserver.yaml.
+ backfill: false
+ # Whether to use custom puppet for backfilling.
+ # In order to use this, the custom puppets must be in the appservice's user ID namespace.
+ double_puppet_backfill: false
# Whether or not puppet avatars should be fetched from the server even if an avatar is already set.
- # If you get 599 errors often, you should try disabling this.
user_avatar_sync: true
# Whether or not Matrix users leaving groups should be bridged to WhatsApp
bridge_matrix_leave: true
- # Maximum number of seconds since last message in chat to skip
- # syncing the chat in any case. This setting will take priority
- # over both recovery_chat_sync_limit and initial_chat_sync_count.
- # 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.
@@ -169,20 +129,12 @@ bridge:
# manually.
login_shared_secret: null
- # Whether or not to invite own WhatsApp user's Matrix puppet into private
- # chat portals when backfilling if needed.
- # This always uses the default puppet instead of custom puppets due to
- # rate limits and timestamp massaging.
- invite_own_puppet_for_backfilling: true
- # Whether or not to explicitly set the avatar and room name for private
- # chat portal rooms. This can be useful if the previous field works fine,
- # but causes room avatar/name bugs.
+ # Whether to explicitly set the avatar and room name for private chat portal rooms.
private_chat_portal_meta: false
- # Whether or not Matrix m.notice-type messages should be bridged.
+ # Whether Matrix m.notice-type messages should be bridged.
bridge_notices: true
# Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.
- # This field will automatically be changed back to false after it,
- # except if the config file is not writable.
+ # This field will automatically be changed back to false after it, except if the config file is not writable.
resend_bridge_info: false
# When using double puppeting, should muted chats be muted in Matrix?
mute_bridging: false
@@ -246,7 +198,7 @@ bridge:
# Permissions for using the bridge.
# Permitted values:
- # relaybot - Talk through the relaybot (if enabled), no access otherwise
+ # relay - Talk through the relaybot (if enabled), no access otherwise
# user - Access to use the bridge to chat with a WhatsApp account.
# admin - User level and some additional administration tools
# Permitted keys:
@@ -254,19 +206,16 @@ bridge:
# domain - All users on that homeserver
# mxid - Specific user
permissions:
- "*": relaybot
+ "*": relay
"example.com": user
"@admin:example.com": admin
- relaybot:
- # Whether or not relaybot support is enabled.
+ relay:
+ # Whether relay mode should be allowed. If allowed, `!signal set-relay` can be used to turn any
+ # authenticated user into a relaybot for that chat.
enabled: false
- # The management room for the bot. This is where all status notifications are posted and
- # in this room, you can use `!wa ` instead of `!wa relaybot `. Omitting
- # the command prefix completely like in user management rooms is not possible.
- management: "!foo:example.com"
- # List of users to invite to all created rooms that include the relaybot.
- invites: []
+ # Should only admins be allowed to set themselves as relay users?
+ admin_only: true
# The formats to use when sending messages to WhatsApp via the relaybot.
message_formats:
m.text: "{{ .Sender.Displayname }}: {{ .Message }}"
diff --git a/formatting.go b/formatting.go
index 3b3237e..f1afac7 100644
--- a/formatting.go
+++ b/formatting.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -22,7 +22,7 @@ import (
"regexp"
"strings"
- "github.com/Rhymen/go-whatsapp"
+ "go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
@@ -57,32 +57,22 @@ func NewFormatter(bridge *Bridge) *Formatter {
if mxid[0] == '@' {
puppet := bridge.GetPuppetByMXID(id.UserID(mxid))
if puppet != nil {
- jids, ok := ctx[mentionedJIDsContextKey].([]whatsapp.JID)
+ jids, ok := ctx[mentionedJIDsContextKey].([]string)
if !ok {
- ctx[mentionedJIDsContextKey] = []whatsapp.JID{puppet.JID}
+ ctx[mentionedJIDsContextKey] = []string{puppet.JID.String()}
} else {
- ctx[mentionedJIDsContextKey] = append(jids, puppet.JID)
+ ctx[mentionedJIDsContextKey] = append(jids, puppet.JID.String())
}
- return "@" + puppet.PhoneNumber()
+ return "@" + puppet.JID.User
}
}
return mxid
},
- BoldConverter: func(text string, _ format.Context) string {
- return fmt.Sprintf("*%s*", text)
- },
- ItalicConverter: func(text string, _ format.Context) string {
- return fmt.Sprintf("_%s_", text)
- },
- StrikethroughConverter: func(text string, _ format.Context) string {
- return fmt.Sprintf("~%s~", text)
- },
- MonospaceConverter: func(text string, _ format.Context) string {
- return fmt.Sprintf("```%s```", text)
- },
- MonospaceBlockConverter: func(text, language string, _ format.Context) string {
- return fmt.Sprintf("```%s```", text)
- },
+ BoldConverter: func(text string, _ format.Context) string { return fmt.Sprintf("*%s*", text) },
+ ItalicConverter: func(text string, _ format.Context) string { return fmt.Sprintf("_%s_", text) },
+ StrikethroughConverter: func(text string, _ format.Context) string { return fmt.Sprintf("~%s~", text) },
+ MonospaceConverter: func(text string, _ format.Context) string { return fmt.Sprintf("```%s```", text) },
+ MonospaceBlockConverter: func(text, language string, _ format.Context) string { return fmt.Sprintf("```%s```", text) },
},
waReplString: map[*regexp.Regexp]string{
italicRegex: "$1$2$3",
@@ -99,12 +89,11 @@ func NewFormatter(bridge *Bridge) *Formatter {
return fmt.Sprintf("%s
", str)
},
}
- formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{
- }
+ formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{}
return formatter
}
-func (formatter *Formatter) getMatrixInfoByJID(jid whatsapp.JID) (mxid id.UserID, displayname string) {
+func (formatter *Formatter) getMatrixInfoByJID(jid types.JID) (mxid id.UserID, displayname string) {
if user := formatter.bridge.GetUserByJID(jid); user != nil {
mxid = user.MXID
displayname = string(user.MXID)
@@ -115,7 +104,7 @@ func (formatter *Formatter) getMatrixInfoByJID(jid whatsapp.JID) (mxid id.UserID
return
}
-func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, mentionedJIDs []whatsapp.JID) {
+func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, mentionedJIDs []string) {
output := html.EscapeString(content.Body)
for regex, replacement := range formatter.waReplString {
output = regex.ReplaceAllString(output, replacement)
@@ -123,14 +112,20 @@ func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, me
for regex, replacer := range formatter.waReplFunc {
output = regex.ReplaceAllStringFunc(output, replacer)
}
- for _, jid := range mentionedJIDs {
+ for _, rawJID := range mentionedJIDs {
+ jid, err := types.ParseJID(rawJID)
+ if err != nil {
+ continue
+ } else if jid.Server == types.LegacyUserServer {
+ jid.Server = types.DefaultUserServer
+ }
mxid, displayname := formatter.getMatrixInfoByJID(jid)
- number := "@" + strings.Replace(jid, whatsapp.NewUserSuffix, "", 1)
- output = strings.Replace(output, number, fmt.Sprintf(`%s`, mxid, displayname), -1)
- content.Body = strings.Replace(content.Body, number, displayname, -1)
+ number := "@" + jid.User
+ output = strings.ReplaceAll(output, number, fmt.Sprintf(`%s`, mxid, displayname))
+ content.Body = strings.ReplaceAll(content.Body, number, displayname)
}
if output != content.Body {
- output = strings.Replace(output, "\n", "
", -1)
+ output = strings.ReplaceAll(output, "\n", "
")
content.FormattedBody = output
content.Format = event.FormatHTML
for regex, replacer := range formatter.waReplFuncText {
@@ -139,9 +134,9 @@ func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, me
}
}
-func (formatter *Formatter) ParseMatrix(html string) (string, []whatsapp.JID) {
+func (formatter *Formatter) ParseMatrix(html string) (string, []string) {
ctx := make(format.Context)
result := formatter.matrixHTMLParser.Parse(html, ctx)
- mentionedJIDs, _ := ctx[mentionedJIDsContextKey].([]whatsapp.JID)
+ mentionedJIDs, _ := ctx[mentionedJIDsContextKey].([]string)
return result, mentionedJIDs
}
diff --git a/go.mod b/go.mod
index ec181cb..973d708 100644
--- a/go.mod
+++ b/go.mod
@@ -1,19 +1,40 @@
module maunium.net/go/mautrix-whatsapp
-go 1.14
+go 1.17
require (
- github.com/Rhymen/go-whatsapp v0.1.0
github.com/gorilla/websocket v1.4.2
- github.com/lib/pq v1.10.2
- github.com/mattn/go-sqlite3 v1.14.8
+ github.com/lib/pq v1.10.3
+ github.com/mattn/go-sqlite3 v1.14.9
github.com/prometheus/client_golang v1.11.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
+ go.mau.fi/whatsmeow v0.0.0-20211029221633-b2fb3fda9a8c
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
+ google.golang.org/protobuf v1.27.1
gopkg.in/yaml.v2 v2.4.0
maunium.net/go/mauflag v1.0.0
- maunium.net/go/maulogger/v2 v2.3.0
- maunium.net/go/mautrix v0.9.27
+ maunium.net/go/maulogger/v2 v2.3.1
+ maunium.net/go/mautrix v0.9.31
)
-replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.5.12
+require (
+ filippo.io/edwards25519 v1.0.0-rc.1 // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/btcsuite/btcutil v1.0.2 // indirect
+ github.com/cespare/xxhash/v2 v2.1.1 // indirect
+ github.com/golang/protobuf v1.5.0 // indirect
+ github.com/gorilla/mux v1.8.0 // indirect
+ github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
+ github.com/prometheus/client_model v0.2.0 // indirect
+ github.com/prometheus/common v0.26.0 // indirect
+ github.com/prometheus/procfs v0.6.0 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/tidwall/gjson v1.10.2 // indirect
+ github.com/tidwall/match v1.1.1 // indirect
+ github.com/tidwall/pretty v1.2.0 // indirect
+ github.com/tidwall/sjson v1.2.3 // indirect
+ go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2 // indirect
+ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
+ golang.org/x/net v0.0.0-20211020060615-d418f374d309 // indirect
+ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
+)
diff --git a/go.sum b/go.sum
index e301303..785b073 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,6 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
+filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -44,9 +46,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -76,12 +77,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
-github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
-github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
-github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
+github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
+github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -128,26 +127,27 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w=
-github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
-github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
-github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
-github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
-github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
-github.com/tidwall/sjson v1.1.5 h1:wsUceI/XDyZk3J1FUvuuYlK62zJv2HO2Pzb8A5EWdUE=
-github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE=
-github.com/tulir/go-whatsapp v0.5.12 h1:JGU5yhoh+CyDcSMUilwy7FL0gFo0zqqepsHRqEjrjKc=
-github.com/tulir/go-whatsapp v0.5.12/go.mod h1:7J3IIL3bEQiBJGtiZst1N4PgXHlWIartdVQLe6lcx9A=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
+github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.3 h1:5+deguEhHSEjmuICXZ21uSSsXotWMA0orU783+Z7Cp8=
+github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs=
+go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2 h1:xpQTMgJGGaF+c8jV/LA/FVXAPJxZbSAGeflOc+Ly6uQ=
+go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2/go.mod h1:3XlVlwOfp8f9Wri+C1D4ORqgUsN4ZvunJOoPjQMBhos=
+go.mau.fi/whatsmeow v0.0.0-20211029221633-b2fb3fda9a8c h1:ZmmT3L8pMKLW3JhcP6Rt0dJg09N+20a8fROxr8MUKzg=
+go.mau.fi/whatsmeow v0.0.0-20211029221633-b2fb3fda9a8c/go.mod h1:ODEmmqeUn9eBDQHFc1S902YA3YFLtmaBujYRRFl53jI=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o=
-golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -157,9 +157,9 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211020060615-d418f374d309 h1:A0lJIi+hcTR6aajJH4YqKWwohY4aW9RO7oRMcdv+HKI=
+golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -172,16 +172,16 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -198,8 +198,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
@@ -217,8 +217,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
-maunium.net/go/maulogger/v2 v2.2.4/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
-maunium.net/go/maulogger/v2 v2.3.0 h1:TMCcO65fLk6+pJXo7sl38tzjzW0KBFgc6JWJMBJp4GE=
-maunium.net/go/maulogger/v2 v2.3.0/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
-maunium.net/go/mautrix v0.9.27 h1:6MV6YSCGqfw8Rb0G1PHjTOkYkTY0vcZaz6wd+U+V1Is=
-maunium.net/go/mautrix v0.9.27/go.mod h1:7IzKfWvpQtN+W2Lzxc0rLvIxFM3ryKX6Ys3S/ZoWbg8=
+maunium.net/go/maulogger/v2 v2.3.1 h1:fwBYJne0pHvJrrIPHK+TAPfyxxbBEz46oVGez2x0ODE=
+maunium.net/go/maulogger/v2 v2.3.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
+maunium.net/go/mautrix v0.9.31 h1:n7UF5tqq2zCyfdNsv++RyQ2anjjrFVOmOA2VkZCSgZc=
+maunium.net/go/mautrix v0.9.31/go.mod h1:3U7pOAx4bxdIVJuunLDAToI+M7YwxcGMm74zBmX5aY0=
diff --git a/main.go b/main.go
index 9432d0a..5a881ee 100644
--- a/main.go
+++ b/main.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -21,12 +21,18 @@ import (
"fmt"
"os"
"os/signal"
+ "strconv"
"strings"
"sync"
"syscall"
"time"
- "github.com/Rhymen/go-whatsapp"
+ "google.golang.org/protobuf/proto"
+
+ waProto "go.mau.fi/whatsmeow/binary/proto"
+ "go.mau.fi/whatsmeow/store"
+ "go.mau.fi/whatsmeow/store/sqlstore"
+ "go.mau.fi/whatsmeow/types"
flag "maunium.net/go/mauflag"
log "maunium.net/go/maulogger/v2"
@@ -41,21 +47,29 @@ import (
"maunium.net/go/mautrix-whatsapp/database/upgrades"
)
+// The name and repo URL of the bridge.
var (
- // These are static
Name = "mautrix-whatsapp"
URL = "https://github.com/mautrix/whatsapp"
- // This is changed when making a release
- Version = "0.1.9"
- // This is filled by init()
- WAVersion = ""
- VersionString = ""
- // These are filled at build time with the -X linker flag
+)
+
+// Information to find out exactly which commit the bridge was built from.
+// These are filled at build time with the -X linker flag.
+var (
Tag = "unknown"
Commit = "unknown"
BuildTime = "unknown"
)
+var (
+ // Version is the version number of the bridge. Changed manually when making a release.
+ Version = "0.2.0+dev"
+ // WAVersion is the version number exposed to WhatsApp. Filled in init()
+ WAVersion = ""
+ // VersionString is the bridge version, plus commit information. Filled in init() using the build-time values.
+ VersionString = ""
+)
+
func init() {
if len(Tag) > 0 && Tag[0] == 'v' {
Tag = Tag[1:]
@@ -145,19 +159,19 @@ type Bridge struct {
Provisioning *ProvisioningAPI
Bot *appservice.IntentAPI
Formatter *Formatter
- Relaybot *User
Crypto Crypto
Metrics *MetricsHandler
+ WAContainer *sqlstore.Container
usersByMXID map[id.UserID]*User
- usersByJID map[whatsapp.JID]*User
+ usersByUsername map[string]*User
usersLock sync.Mutex
managementRooms map[id.RoomID]*User
managementRoomsLock sync.Mutex
portalsByMXID map[id.RoomID]*Portal
portalsByJID map[database.PortalKey]*Portal
portalsLock sync.Mutex
- puppets map[whatsapp.JID]*Puppet
+ puppets map[types.JID]*Puppet
puppetsByCustomMXID map[id.UserID]*Puppet
puppetsLock sync.Mutex
}
@@ -176,11 +190,11 @@ type Crypto interface {
func NewBridge() *Bridge {
bridge := &Bridge{
usersByMXID: make(map[id.UserID]*User),
- usersByJID: make(map[whatsapp.JID]*User),
+ usersByUsername: make(map[string]*User),
managementRooms: make(map[id.RoomID]*User),
portalsByMXID: make(map[id.RoomID]*Portal),
portalsByJID: make(map[database.PortalKey]*Portal),
- puppets: make(map[whatsapp.JID]*Puppet),
+ puppets: make(map[types.JID]*Puppet),
puppetsByCustomMXID: make(map[id.UserID]*Puppet),
}
@@ -259,6 +273,8 @@ func (bridge *Bridge) Init() {
bridge.DB.SetMaxOpenConns(bridge.Config.AppService.Database.MaxOpenConns)
bridge.DB.SetMaxIdleConns(bridge.Config.AppService.Database.MaxIdleConns)
+ bridge.WAContainer = sqlstore.NewWithDB(bridge.DB.DB, bridge.Config.AppService.Database.Type, nil)
+
ss := bridge.Config.AppService.Provisioning.SharedSecret
if len(ss) > 0 && ss != "disable" {
bridge.Provisioning = &ProvisioningAPI{bridge: bridge}
@@ -271,6 +287,23 @@ func (bridge *Bridge) Init() {
bridge.Formatter = NewFormatter(bridge)
bridge.Crypto = NewCryptoHelper(bridge)
bridge.Metrics = NewMetricsHandler(bridge.Config.Metrics.Listen, bridge.Log.Sub("Metrics"), bridge.DB)
+
+ store.BaseClientPayload.UserAgent.OsVersion = proto.String(WAVersion)
+ store.BaseClientPayload.UserAgent.OsBuildNumber = proto.String(WAVersion)
+ store.CompanionProps.Os = proto.String(bridge.Config.WhatsApp.OSName)
+ versionParts := strings.Split(WAVersion, ".")
+ if len(versionParts) > 2 {
+ primary, _ := strconv.Atoi(versionParts[0])
+ secondary, _ := strconv.Atoi(versionParts[1])
+ tertiary, _ := strconv.Atoi(versionParts[2])
+ store.CompanionProps.Version.Primary = proto.Uint32(uint32(primary))
+ store.CompanionProps.Version.Secondary = proto.Uint32(uint32(secondary))
+ store.CompanionProps.Version.Tertiary = proto.Uint32(uint32(tertiary))
+ }
+ platformID, ok := waProto.CompanionProps_CompanionPropsPlatformType_value[strings.ToUpper(bridge.Config.WhatsApp.BrowserName)]
+ if ok {
+ store.CompanionProps.PlatformType = waProto.CompanionProps_CompanionPropsPlatformType(platformID).Enum()
+ }
}
func (bridge *Bridge) Start() {
@@ -289,12 +322,10 @@ func (bridge *Bridge) Start() {
os.Exit(19)
}
}
- bridge.sendGlobalBridgeState(BridgeState{StateEvent: StateStarting}.fill(nil))
if bridge.Provisioning != nil {
bridge.Log.Debugln("Initializing provisioning API")
bridge.Provisioning.Init()
}
- bridge.LoadRelaybot()
bridge.Log.Debugln("Starting application service HTTP server")
go bridge.AS.Start()
bridge.Log.Debugln("Starting event processor")
@@ -327,21 +358,6 @@ func (bridge *Bridge) ResendBridgeInfo() {
bridge.Log.Infoln("Finished re-sending bridge info state events")
}
-func (bridge *Bridge) LoadRelaybot() {
- if !bridge.Config.Bridge.Relaybot.Enabled {
- return
- }
- bridge.Relaybot = bridge.GetUserByMXID("relaybot")
- if bridge.Relaybot.HasSession() {
- bridge.Log.Debugln("Relaybot is enabled")
- } else {
- bridge.Log.Debugln("Relaybot is enabled, but not logged in")
- }
- bridge.Relaybot.ManagementRoom = bridge.Config.Bridge.Relaybot.ManagementRoom
- bridge.Relaybot.IsRelaybot = true
- bridge.Relaybot.Connect(false)
-}
-
func (bridge *Bridge) UpdateBotProfile() {
bridge.Log.Debugln("Updating bot profile")
botConfig := bridge.Config.AppService.Bot
@@ -374,10 +390,10 @@ func (bridge *Bridge) StartUsers() {
bridge.Log.Debugln("Starting users")
foundAnySessions := false
for _, user := range bridge.GetAllUsers() {
- if user.Session != nil {
+ if !user.JID.IsEmpty() {
foundAnySessions = true
}
- go user.Connect(false)
+ go user.Connect()
}
if !foundAnySessions {
bridge.sendGlobalBridgeState(BridgeState{StateEvent: StateUnconfigured}.fill(nil))
@@ -401,15 +417,13 @@ func (bridge *Bridge) Stop() {
bridge.AS.Stop()
bridge.Metrics.Stop()
bridge.EventProcessor.Stop()
- for _, user := range bridge.usersByJID {
- if user.Conn == nil {
+ for _, user := range bridge.usersByUsername {
+ if user.Client == nil {
continue
}
bridge.Log.Debugln("Disconnecting", user.MXID)
- err := user.Conn.Disconnect()
- if err != nil {
- bridge.Log.Errorfln("Error while disconnecting %s: %v", user.MXID, err)
- }
+ user.Client.Disconnect()
+ close(user.historySyncs)
}
}
diff --git a/matrix.go b/matrix.go
index 43c48d4..92869bd 100644
--- a/matrix.go
+++ b/matrix.go
@@ -121,29 +121,9 @@ func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) {
return
}
- if evt.RoomID == mx.bridge.Config.Bridge.Relaybot.ManagementRoom {
- _, _ = intent.SendNotice(evt.RoomID, "This is the relaybot management room. Send `!wa help` to get a list of commands.")
- mx.log.Debugln("Joined relaybot management room", evt.RoomID, "after invite from", evt.Sender)
- return
- }
-
- hasPuppets := false
- for mxid, _ := range members.Joined {
- if mxid == intent.UserID || mxid == evt.Sender {
- continue
- } else if _, ok := mx.bridge.ParsePuppetMXID(mxid); ok {
- hasPuppets = true
- continue
- }
- mx.log.Debugln("Leaving multi-user room", evt.RoomID, "after accepting invite from", evt.Sender)
- _, _ = intent.SendNotice(evt.RoomID, "This bridge is user-specific, please don't invite me into rooms with other users.")
- _, _ = intent.LeaveRoom(evt.RoomID)
- return
- }
-
_, _ = mx.sendNoticeWithMarkdown(evt.RoomID, mx.bridge.Config.Bridge.ManagementRoomText.Welcome)
- if !hasPuppets && (len(user.ManagementRoom) == 0 || evt.Content.AsMember().IsDirect) {
+ if len(members.Joined) == 2 && (len(user.ManagementRoom) == 0 || evt.Content.AsMember().IsDirect) {
user.SetManagementRoom(evt.RoomID)
_, _ = intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room.")
mx.log.Debugln(evt.RoomID, "registered as a management room with", evt.Sender)
@@ -223,13 +203,10 @@ func (mx *MatrixHandler) createPrivatePortalFromInvite(roomID id.RoomID, inviter
portal.UpdateBridgeInfo()
_, _ = intent.SendNotice(roomID, "Private chat portal created")
- err := portal.FillInitialHistory(inviter)
- if err != nil {
- portal.log.Errorln("Failed to fill history:", err)
- }
-
- inviter.addPortalToCommunity(portal)
- inviter.addPuppetToCommunity(puppet)
+ //err := portal.FillInitialHistory(inviter)
+ //if err != nil {
+ // portal.log.Errorln("Failed to fill history:", err)
+ //}
}
func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, puppet *Puppet) {
@@ -281,7 +258,7 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
}
user := mx.bridge.GetUserByMXID(evt.Sender)
- if user == nil || !user.Whitelisted || !user.IsConnected() {
+ if user == nil || !user.Whitelisted || !user.IsLoggedIn() {
return
}
@@ -322,7 +299,7 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) {
}
user := mx.bridge.GetUserByMXID(evt.Sender)
- if user == nil || !user.Whitelisted || !user.IsConnected() {
+ if user == nil || !user.Whitelisted || !user.IsLoggedIn() {
return
}
@@ -343,7 +320,7 @@ func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool {
return true
}
user := mx.bridge.GetUserByMXID(evt.Sender)
- if !user.RelaybotWhitelisted {
+ if !user.RelayWhitelisted {
return true
}
return false
@@ -461,7 +438,7 @@ func (mx *MatrixHandler) HandleRedaction(evt *event.Event) {
if !user.HasSession() {
return
- } else if !user.IsConnected() {
+ } else if !user.IsLoggedIn() {
msg := format.RenderMarkdown(fmt.Sprintf("[%[1]s](https://matrix.to/#/%[1]s): \u26a0 "+
"You are not connected to WhatsApp, so your redaction was not bridged. "+
"Use `%[2]s reconnect` to reconnect.", user.MXID, mx.bridge.Config.Bridge.CommandPrefix), true, false)
diff --git a/metrics.go b/metrics.go
index 064dc11..73d9762 100644
--- a/metrics.go
+++ b/metrics.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -28,7 +28,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
log "maunium.net/go/maulogger/v2"
- "github.com/Rhymen/go-whatsapp"
+ "go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
@@ -59,16 +59,12 @@ type MetricsHandler struct {
unencryptedGroupCount prometheus.Gauge
unencryptedPrivateCount prometheus.Gauge
- connected prometheus.Gauge
- connectedState map[whatsapp.JID]bool
- connectedStateLock sync.Mutex
- loggedIn prometheus.Gauge
- loggedInState map[whatsapp.JID]bool
- loggedInStateLock sync.Mutex
- syncLocked prometheus.Gauge
- syncLockedState map[whatsapp.JID]bool
- syncLockedStateLock sync.Mutex
- bufferLength *prometheus.GaugeVec
+ connected prometheus.Gauge
+ connectedState map[string]bool
+ connectedStateLock sync.Mutex
+ loggedIn prometheus.Gauge
+ loggedInState map[string]bool
+ loggedInStateLock sync.Mutex
}
func NewMetricsHandler(address string, log log.Logger, db *database.Database) *MetricsHandler {
@@ -125,21 +121,12 @@ func NewMetricsHandler(address string, log log.Logger, db *database.Database) *M
Name: "bridge_logged_in",
Help: "Users logged into the bridge",
}),
- loggedInState: make(map[whatsapp.JID]bool),
+ loggedInState: make(map[string]bool),
connected: promauto.NewGauge(prometheus.GaugeOpts{
Name: "bridge_connected",
Help: "Bridge users connected to WhatsApp",
}),
- connectedState: make(map[whatsapp.JID]bool),
- syncLocked: promauto.NewGauge(prometheus.GaugeOpts{
- Name: "bridge_sync_locked",
- Help: "Bridge users locked in post-login sync",
- }),
- syncLockedState: make(map[whatsapp.JID]bool),
- bufferLength: promauto.NewGaugeVec(prometheus.GaugeOpts{
- Name: "bridge_buffer_size",
- Help: "Number of messages in buffer",
- }, []string{"user_id"}),
+ connectedState: make(map[string]bool),
}
}
@@ -158,7 +145,7 @@ func (mh *MetricsHandler) TrackMatrixEvent(eventType event.Type) func() {
}
}
-func (mh *MetricsHandler) TrackWhatsAppMessage(timestamp uint64, messageType string) func() {
+func (mh *MetricsHandler) TrackWhatsAppMessage(timestamp time.Time, messageType string) func() {
if !mh.running {
return noop
}
@@ -169,7 +156,7 @@ func (mh *MetricsHandler) TrackWhatsAppMessage(timestamp uint64, messageType str
mh.whatsappMessageHandling.
With(prometheus.Labels{"message_type": messageType}).
Observe(duration.Seconds())
- mh.whatsappMessageAge.Observe(time.Now().Sub(time.Unix(int64(timestamp), 0)).Seconds())
+ mh.whatsappMessageAge.Observe(time.Now().Sub(timestamp).Seconds())
}
}
@@ -180,15 +167,15 @@ func (mh *MetricsHandler) TrackDisconnection(userID id.UserID) {
mh.disconnections.With(prometheus.Labels{"user_id": string(userID)}).Inc()
}
-func (mh *MetricsHandler) TrackLoginState(jid whatsapp.JID, loggedIn bool) {
+func (mh *MetricsHandler) TrackLoginState(jid types.JID, loggedIn bool) {
if !mh.running {
return
}
mh.loggedInStateLock.Lock()
defer mh.loggedInStateLock.Unlock()
- currentVal, ok := mh.loggedInState[jid]
+ currentVal, ok := mh.loggedInState[jid.User]
if !ok || currentVal != loggedIn {
- mh.loggedInState[jid] = loggedIn
+ mh.loggedInState[jid.User] = loggedIn
if loggedIn {
mh.loggedIn.Inc()
} else {
@@ -197,16 +184,15 @@ func (mh *MetricsHandler) TrackLoginState(jid whatsapp.JID, loggedIn bool) {
}
}
-func (mh *MetricsHandler) TrackConnectionState(jid whatsapp.JID, connected bool) {
+func (mh *MetricsHandler) TrackConnectionState(jid types.JID, connected bool) {
if !mh.running {
return
}
-
mh.connectedStateLock.Lock()
defer mh.connectedStateLock.Unlock()
- currentVal, ok := mh.connectedState[jid]
+ currentVal, ok := mh.connectedState[jid.User]
if !ok || currentVal != connected {
- mh.connectedState[jid] = connected
+ mh.connectedState[jid.User] = connected
if connected {
mh.connected.Inc()
} else {
@@ -215,30 +201,6 @@ func (mh *MetricsHandler) TrackConnectionState(jid whatsapp.JID, connected bool)
}
}
-func (mh *MetricsHandler) TrackSyncLock(jid whatsapp.JID, locked bool) {
- if !mh.running {
- return
- }
- mh.syncLockedStateLock.Lock()
- defer mh.syncLockedStateLock.Unlock()
- currentVal, ok := mh.syncLockedState[jid]
- if !ok || currentVal != locked {
- mh.syncLockedState[jid] = locked
- if locked {
- mh.syncLocked.Inc()
- } else {
- mh.syncLocked.Dec()
- }
- }
-}
-
-func (mh *MetricsHandler) TrackBufferLength(id id.UserID, length int) {
- if !mh.running {
- return
- }
- mh.bufferLength.With(prometheus.Labels{"user_id": string(id)}).Set(float64(length))
-}
-
func (mh *MetricsHandler) updateStats() {
start := time.Now()
var puppetCount int
diff --git a/no-crypto.go b/no-crypto.go
index 75a2c68..14e49c0 100644
--- a/no-crypto.go
+++ b/no-crypto.go
@@ -1,3 +1,4 @@
+//go:build !cgo || nocrypto
// +build !cgo nocrypto
package main
diff --git a/portal.go b/portal.go
index a28eb47..566e746 100644
--- a/portal.go
+++ b/portal.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -18,8 +18,8 @@ package main
import (
"bytes"
+ "context"
"encoding/gob"
- "encoding/hex"
"errors"
"fmt"
"html"
@@ -29,22 +29,25 @@ import (
"image/png"
"io/ioutil"
"math"
- "math/rand"
"mime"
"net/http"
"os"
"os/exec"
"path/filepath"
- "reflect"
"strconv"
"strings"
"sync"
- "sync/atomic"
"time"
"golang.org/x/image/webp"
- "github.com/Rhymen/go-whatsapp"
- waProto "github.com/Rhymen/go-whatsapp/binary/proto"
+ "google.golang.org/protobuf/proto"
+
+ "maunium.net/go/mautrix/format"
+
+ "go.mau.fi/whatsmeow"
+ waProto "go.mau.fi/whatsmeow/binary/proto"
+ "go.mau.fi/whatsmeow/types"
+ "go.mau.fi/whatsmeow/types/events"
log "maunium.net/go/maulogger/v2"
@@ -52,9 +55,7 @@ import (
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
- "maunium.net/go/mautrix/pushrules"
"maunium.net/go/mautrix-whatsapp/database"
)
@@ -91,7 +92,7 @@ func (bridge *Bridge) GetAllPortals() []*Portal {
return bridge.dbPortalsToPortals(bridge.DB.Portal.GetAll())
}
-func (bridge *Bridge) GetAllPortalsByJID(jid whatsapp.JID) []*Portal {
+func (bridge *Bridge) GetAllPortalsByJID(jid types.JID) []*Portal {
return bridge.dbPortalsToPortals(bridge.DB.Portal.GetAllByJID(jid))
}
@@ -139,8 +140,6 @@ func (bridge *Bridge) NewManualPortal(key database.PortalKey) *Portal {
bridge: bridge,
log: bridge.Log.Sub(fmt.Sprintf("Portal/%s", key)),
- recentlyHandled: [recentlyHandledLength]whatsapp.MessageID{},
-
messages: make(chan PortalMessage, bridge.Config.Bridge.PortalMessageBuffer),
}
portal.Key = key
@@ -154,8 +153,6 @@ func (bridge *Bridge) NewPortal(dbPortal *database.Portal) *Portal {
bridge: bridge,
log: bridge.Log.Sub(fmt.Sprintf("Portal/%s", dbPortal.Key)),
- recentlyHandled: [recentlyHandledLength]whatsapp.MessageID{},
-
messages: make(chan PortalMessage, bridge.Config.Bridge.PortalMessageBuffer),
}
go portal.handleMessageLoop()
@@ -165,10 +162,14 @@ func (bridge *Bridge) NewPortal(dbPortal *database.Portal) *Portal {
const recentlyHandledLength = 100
type PortalMessage struct {
- chat string
- source *User
- data interface{}
- timestamp uint64
+ evt *events.Message
+ undecryptable *events.UndecryptableMessage
+ source *User
+}
+
+type recentlyHandledWrapper struct {
+ id types.MessageID
+ err bool
}
type Portal struct {
@@ -179,51 +180,23 @@ type Portal struct {
roomCreateLock sync.Mutex
encryptLock sync.Mutex
+ backfillLock sync.Mutex
- recentlyHandled [recentlyHandledLength]whatsapp.MessageID
+ recentlyHandled [recentlyHandledLength]recentlyHandledWrapper
recentlyHandledLock sync.Mutex
recentlyHandledIndex uint8
- backfillLock sync.Mutex
- backfilling bool
- lastMessageTs uint64
-
privateChatBackfillInvitePuppet func()
messages chan PortalMessage
- isPrivate *bool
- isBroadcast *bool
- hasRelaybot *bool
-}
-
-const MaxMessageAgeToCreatePortal = 5 * 60 // 5 minutes
-
-func (portal *Portal) syncDoublePuppetDetailsAfterCreate(source *User) {
- doublePuppet := portal.bridge.GetPuppetByCustomMXID(source.MXID)
- if doublePuppet == nil {
- return
- }
- source.Conn.Store.ChatsLock.RLock()
- chat, ok := source.Conn.Store.Chats[portal.Key.JID]
- source.Conn.Store.ChatsLock.RUnlock()
- if !ok {
- portal.log.Debugln("Not syncing chat mute/tags with %s: chat info not found", source.MXID)
- return
- }
- source.syncChatDoublePuppetDetails(doublePuppet, Chat{
- Chat: chat,
- Portal: portal,
- }, true)
+ relayUser *User
}
func (portal *Portal) handleMessageLoop() {
for msg := range portal.messages {
if len(portal.MXID) == 0 {
- if msg.timestamp+MaxMessageAgeToCreatePortal < uint64(time.Now().Unix()) {
- portal.log.Debugln("Not creating portal room for incoming message: message is too old")
- continue
- } else if !portal.shouldCreateRoom(msg) {
+ if !portal.shouldCreateRoom(msg) {
portal.log.Debugln("Not creating portal room for incoming message: message is not a chat message")
continue
}
@@ -233,103 +206,205 @@ func (portal *Portal) handleMessageLoop() {
portal.log.Errorln("Failed to create portal room:", err)
continue
}
- portal.syncDoublePuppetDetailsAfterCreate(msg.source)
}
- portal.backfillLock.Lock()
- portal.handleMessage(msg, false)
- portal.backfillLock.Unlock()
+ if msg.evt != nil {
+ portal.handleMessage(msg.source, msg.evt)
+ } else if msg.undecryptable != nil {
+ portal.handleUndecryptableMessage(msg.source, msg.undecryptable)
+ } else {
+ portal.log.Warnln("Unexpected PortalMessage with no message: %+v", msg)
+ }
}
}
func (portal *Portal) shouldCreateRoom(msg PortalMessage) bool {
- stubMsg, ok := msg.data.(whatsapp.StubMessage)
- if ok {
- // This could be more specific: if someone else was added, we might not care,
- // but if the local user was added, we definitely care.
- return stubMsg.Type == waProto.WebMessageInfo_GROUP_PARTICIPANT_ADD || stubMsg.Type == waProto.WebMessageInfo_GROUP_PARTICIPANT_INVITE
+ if msg.undecryptable != nil {
+ return true
}
- return true
-}
-
-func (portal *Portal) handleMessage(msg PortalMessage, isBackfill bool) {
- if len(portal.MXID) == 0 {
- portal.log.Warnln("handleMessage called even though portal.MXID is empty")
- return
+ waMsg := msg.evt.Message
+ supportedMessages := []interface{}{
+ waMsg.Conversation,
+ waMsg.ExtendedTextMessage,
+ waMsg.ImageMessage,
+ waMsg.StickerMessage,
+ waMsg.VideoMessage,
+ waMsg.AudioMessage,
+ waMsg.VideoMessage,
+ waMsg.DocumentMessage,
+ waMsg.ContactMessage,
+ waMsg.LocationMessage,
}
- var triedToHandle bool
- var trackMessageCallback func()
- dataType := reflect.TypeOf(msg.data)
- if !isBackfill {
- trackMessageCallback = portal.bridge.Metrics.TrackWhatsAppMessage(msg.timestamp, dataType.Name())
- }
- switch data := msg.data.(type) {
- case whatsapp.TextMessage:
- triedToHandle = portal.HandleTextMessage(msg.source, data)
- case whatsapp.ImageMessage:
- triedToHandle = portal.HandleMediaMessage(msg.source, mediaMessage{
- base: base{data.Download, data.Info, data.ContextInfo, data.Type},
- thumbnail: data.Thumbnail,
- caption: data.Caption,
- })
- case whatsapp.StickerMessage:
- triedToHandle = portal.HandleMediaMessage(msg.source, mediaMessage{
- base: base{data.Download, data.Info, data.ContextInfo, data.Type},
- sendAsSticker: true,
- })
- case whatsapp.VideoMessage:
- triedToHandle = portal.HandleMediaMessage(msg.source, mediaMessage{
- base: base{data.Download, data.Info, data.ContextInfo, data.Type},
- thumbnail: data.Thumbnail,
- caption: data.Caption,
- length: data.Length * 1000,
- })
- case whatsapp.AudioMessage:
- triedToHandle = portal.HandleMediaMessage(msg.source, mediaMessage{
- base: base{data.Download, data.Info, data.ContextInfo, data.Type},
- length: data.Length * 1000,
- })
- case whatsapp.DocumentMessage:
- fileName := data.FileName
- if len(fileName) == 0 {
- fileName = data.Title
- }
- triedToHandle = portal.HandleMediaMessage(msg.source, mediaMessage{
- base: base{data.Download, data.Info, data.ContextInfo, data.Type},
- thumbnail: data.Thumbnail,
- fileName: fileName,
- })
- case whatsapp.ContactMessage:
- triedToHandle = portal.HandleContactMessage(msg.source, data)
- case whatsapp.LocationMessage:
- triedToHandle = portal.HandleLocationMessage(msg.source, data)
- case whatsapp.StubMessage:
- triedToHandle = portal.HandleStubMessage(msg.source, data, isBackfill)
- case whatsapp.MessageRevocation:
- triedToHandle = portal.HandleMessageRevoke(msg.source, data)
- case FakeMessage:
- triedToHandle = portal.HandleFakeMessage(msg.source, data)
- default:
- portal.log.Warnln("Unknown message type:", dataType)
- }
- if triedToHandle && trackMessageCallback != nil {
- trackMessageCallback()
- }
-}
-
-func (portal *Portal) isRecentlyHandled(id whatsapp.MessageID) bool {
- start := portal.recentlyHandledIndex
- for i := start; i != start; i = (i - 1) % recentlyHandledLength {
- if portal.recentlyHandled[i] == id {
+ for _, message := range supportedMessages {
+ if message != nil {
return true
}
}
return false
}
-func (portal *Portal) isDuplicate(id whatsapp.MessageID) bool {
- msg := portal.bridge.DB.Message.GetByJID(portal.Key, id)
- if msg != nil {
- return true
+func (portal *Portal) getMessageType(waMsg *waProto.Message) string {
+ switch {
+ case waMsg == nil:
+ return "ignore"
+ case waMsg.Conversation != nil || waMsg.ExtendedTextMessage != nil:
+ return "text"
+ case waMsg.ImageMessage != nil:
+ return fmt.Sprintf("image %s", waMsg.GetImageMessage().GetMimetype())
+ case waMsg.StickerMessage != nil:
+ return fmt.Sprintf("sticker %s", waMsg.GetStickerMessage().GetMimetype())
+ case waMsg.VideoMessage != nil:
+ return fmt.Sprintf("video %s", waMsg.GetVideoMessage().GetMimetype())
+ case waMsg.AudioMessage != nil:
+ return fmt.Sprintf("audio %s", waMsg.GetAudioMessage().GetMimetype())
+ case waMsg.DocumentMessage != nil:
+ return fmt.Sprintf("document %s", waMsg.GetDocumentMessage().GetMimetype())
+ case waMsg.ContactMessage != nil:
+ return "contact"
+ case waMsg.LocationMessage != nil:
+ return "location"
+ case waMsg.GetProtocolMessage() != nil:
+ switch waMsg.GetProtocolMessage().GetType() {
+ case waProto.ProtocolMessage_REVOKE:
+ return "revoke"
+ case waProto.ProtocolMessage_APP_STATE_SYNC_KEY_SHARE, waProto.ProtocolMessage_HISTORY_SYNC_NOTIFICATION, waProto.ProtocolMessage_INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC:
+ return "ignore"
+ default:
+ return "unknown_protocol"
+ }
+ default:
+ return "unknown"
+ }
+}
+
+func (portal *Portal) convertMessage(intent *appservice.IntentAPI, source *User, info *types.MessageInfo, waMsg *waProto.Message) *ConvertedMessage {
+ switch {
+ case waMsg.Conversation != nil || waMsg.ExtendedTextMessage != nil:
+ return portal.convertTextMessage(intent, waMsg)
+ case waMsg.ImageMessage != nil:
+ return portal.convertMediaMessage(intent, source, info, waMsg.GetImageMessage())
+ case waMsg.StickerMessage != nil:
+ return portal.convertMediaMessage(intent, source, info, waMsg.GetStickerMessage())
+ case waMsg.VideoMessage != nil:
+ return portal.convertMediaMessage(intent, source, info, waMsg.GetVideoMessage())
+ case waMsg.AudioMessage != nil:
+ return portal.convertMediaMessage(intent, source, info, waMsg.GetAudioMessage())
+ case waMsg.DocumentMessage != nil:
+ return portal.convertMediaMessage(intent, source, info, waMsg.GetDocumentMessage())
+ case waMsg.ContactMessage != nil:
+ return portal.convertContactMessage(intent, waMsg.GetContactMessage())
+ case waMsg.LocationMessage != nil:
+ return portal.convertLocationMessage(intent, waMsg.GetLocationMessage())
+ default:
+ return nil
+ }
+}
+
+const UndecryptableMessageNotice = "Decrypting message from WhatsApp failed, waiting for sender to re-send... " +
+ "([learn more](https://faq.whatsapp.com/general/security-and-privacy/seeing-waiting-for-this-message-this-may-take-a-while))"
+
+var undecryptableMessageContent event.MessageEventContent
+
+func init() {
+ undecryptableMessageContent = format.RenderMarkdown(UndecryptableMessageNotice, true, false)
+ undecryptableMessageContent.MsgType = event.MsgNotice
+}
+
+func (portal *Portal) handleUndecryptableMessage(source *User, evt *events.UndecryptableMessage) {
+ if len(portal.MXID) == 0 {
+ portal.log.Warnln("handleUndecryptableMessage called even though portal.MXID is empty")
+ return
+ } else if portal.isRecentlyHandled(evt.Info.ID, true) {
+ portal.log.Debugfln("Not handling %s (undecryptable): message was recently handled", evt.Info.ID)
+ return
+ } else if existingMsg := portal.bridge.DB.Message.GetByJID(portal.Key, evt.Info.ID); existingMsg != nil {
+ portal.log.Debugfln("Not handling %s (undecryptable): message is duplicate", evt.Info.ID)
+ return
+ }
+ intent := portal.getMessageIntent(source, &evt.Info)
+ content := undecryptableMessageContent
+ resp, err := portal.sendMessage(intent, event.EventMessage, &content, evt.Info.Timestamp.UnixMilli())
+ if err != nil {
+ portal.log.Errorln("Failed to send decryption error of %s to Matrix: %v", evt.Info.ID, err)
+ }
+ portal.finishHandling(nil, &evt.Info, resp.EventID, true)
+}
+
+func (portal *Portal) handleMessage(source *User, evt *events.Message) {
+ if len(portal.MXID) == 0 {
+ portal.log.Warnln("handleMessage called even though portal.MXID is empty")
+ return
+ }
+ msgID := evt.Info.ID
+ msgType := portal.getMessageType(evt.Message)
+ if msgType == "ignore" {
+ return
+ } else if portal.isRecentlyHandled(msgID, false) {
+ portal.log.Debugfln("Not handling %s (%s): message was recently handled", msgID, msgType)
+ return
+ }
+ existingMsg := portal.bridge.DB.Message.GetByJID(portal.Key, msgID)
+ if existingMsg != nil {
+ if existingMsg.DecryptionError {
+ portal.log.Debugfln("Got decryptable version of previously undecryptable message %s (%s)", msgID, msgType)
+ } else {
+ portal.log.Debugfln("Not handling %s (%s): message is duplicate", msgID, msgType)
+ return
+ }
+ }
+
+ intent := portal.getMessageIntent(source, &evt.Info)
+ converted := portal.convertMessage(intent, source, &evt.Info, evt.Message)
+ if converted != nil {
+ var eventID id.EventID
+ if existingMsg != nil {
+ converted.Content.SetEdit(existingMsg.MXID)
+ }
+ resp, err := portal.sendMessage(converted.Intent, converted.Type, converted.Content, evt.Info.Timestamp.UnixMilli())
+ if err != nil {
+ portal.log.Errorln("Failed to send %s to Matrix: %v", msgID, err)
+ } else {
+ eventID = resp.EventID
+ }
+ // TODO figure out how to handle captions with undecryptable messages turning decryptable
+ if converted.Caption != nil && existingMsg == nil {
+ resp, err = portal.sendMessage(converted.Intent, converted.Type, converted.Content, evt.Info.Timestamp.UnixMilli())
+ if err != nil {
+ portal.log.Errorln("Failed to send caption of %s to Matrix: %v", msgID, err)
+ } else {
+ eventID = resp.EventID
+ }
+ }
+ if len(eventID) != 0 {
+ portal.finishHandling(existingMsg, &evt.Info, resp.EventID, false)
+ }
+ } else if msgType == "revoke" {
+ portal.HandleMessageRevoke(source, &evt.Info, evt.Message.GetProtocolMessage().GetKey())
+ if existingMsg != nil {
+ _, _ = portal.MainIntent().RedactEvent(portal.MXID, existingMsg.MXID, mautrix.ReqRedact{
+ Reason: "The undecryptable message was actually the deletion of another message",
+ })
+ existingMsg.UpdateMXID("net.maunium.whatsapp.fake::"+existingMsg.MXID, false)
+ }
+ } else {
+ portal.log.Warnfln("Unhandled message: %+v / %+v", evt.Info, evt.Message)
+ if existingMsg != nil {
+ _, _ = portal.MainIntent().RedactEvent(portal.MXID, existingMsg.MXID, mautrix.ReqRedact{
+ Reason: "The undecryptable message contained an unsupported message type",
+ })
+ existingMsg.UpdateMXID("net.maunium.whatsapp.fake::"+existingMsg.MXID, false)
+ }
+ return
+ }
+ portal.bridge.Metrics.TrackWhatsAppMessage(evt.Info.Timestamp, strings.Split(msgType, " ")[0])
+}
+
+func (portal *Portal) isRecentlyHandled(id types.MessageID, decryptionError bool) bool {
+ start := portal.recentlyHandledIndex
+ lookingForMsg := recentlyHandledWrapper{id, decryptionError}
+ for i := start; i != start; i = (i - 1) % recentlyHandledLength {
+ if portal.recentlyHandled[i] == lookingForMsg {
+ return true
+ }
}
return false
}
@@ -338,148 +413,127 @@ func init() {
gob.Register(&waProto.Message{})
}
-func (portal *Portal) markHandled(source *User, message *waProto.WebMessageInfo, mxid id.EventID, isSent bool) *database.Message {
- msg := portal.bridge.DB.Message.New()
- msg.Chat = portal.Key
- msg.JID = message.GetKey().GetId()
- msg.MXID = mxid
- msg.Timestamp = int64(message.GetMessageTimestamp())
- if message.GetKey().GetFromMe() {
- msg.Sender = source.JID
- } else if portal.IsPrivateChat() {
- msg.Sender = portal.Key.JID
+func (portal *Portal) markHandled(msg *database.Message, info *types.MessageInfo, mxid id.EventID, isSent, recent, decryptionError bool) *database.Message {
+ if msg == nil {
+ msg = portal.bridge.DB.Message.New()
+ msg.Chat = portal.Key
+ msg.JID = info.ID
+ msg.MXID = mxid
+ msg.Timestamp = info.Timestamp
+ msg.Sender = info.Sender
+ msg.Sent = isSent
+ msg.DecryptionError = decryptionError
+ msg.Insert()
} else {
- msg.Sender = message.GetKey().GetParticipant()
- if len(msg.Sender) == 0 {
- msg.Sender = message.GetParticipant()
- }
+ msg.UpdateMXID(mxid, decryptionError)
}
- msg.Sent = isSent
- msg.Insert()
- portal.recentlyHandledLock.Lock()
- index := portal.recentlyHandledIndex
- portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength
- portal.recentlyHandledLock.Unlock()
- portal.recentlyHandled[index] = msg.JID
+ if recent {
+ portal.recentlyHandledLock.Lock()
+ index := portal.recentlyHandledIndex
+ portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength
+ portal.recentlyHandledLock.Unlock()
+ portal.recentlyHandled[index] = recentlyHandledWrapper{msg.JID, decryptionError}
+ }
return msg
}
-func (portal *Portal) getMessageIntent(user *User, info whatsapp.MessageInfo) *appservice.IntentAPI {
- if info.FromMe {
+func (portal *Portal) getMessagePuppet(user *User, info *types.MessageInfo) *Puppet {
+ if info.IsFromMe {
+ return portal.bridge.GetPuppetByJID(user.JID)
+ } else if portal.IsPrivateChat() {
+ return portal.bridge.GetPuppetByJID(portal.Key.JID)
+ } else {
+ puppet := portal.bridge.GetPuppetByJID(info.Sender)
+ puppet.SyncContact(user, true)
+ return puppet
+ }
+}
+
+func (portal *Portal) getMessageIntent(user *User, info *types.MessageInfo) *appservice.IntentAPI {
+ if info.IsFromMe {
return portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal)
} else if portal.IsPrivateChat() {
return portal.MainIntent()
- } else if len(info.SenderJid) == 0 {
- if len(info.Source.GetParticipant()) != 0 {
- info.SenderJid = info.Source.GetParticipant()
- } else {
- return nil
- }
}
- puppet := portal.bridge.GetPuppetByJID(info.SenderJid)
- puppet.SyncContactIfNecessary(user)
+ puppet := portal.bridge.GetPuppetByJID(info.Sender)
+ puppet.SyncContact(user, true)
return puppet.IntentFor(portal)
}
-func (portal *Portal) startHandling(source *User, info whatsapp.MessageInfo, msgType string) *appservice.IntentAPI {
- // TODO these should all be trace logs
- if portal.lastMessageTs == 0 {
- portal.log.Debugln("Fetching last message from database to get its timestamp")
- lastMessage := portal.bridge.DB.Message.GetLastInChat(portal.Key)
- if lastMessage != nil {
- atomic.CompareAndSwapUint64(&portal.lastMessageTs, 0, uint64(lastMessage.Timestamp))
- }
- }
-
- // If there are messages slightly older than the last message, it's possible the order is just wrong,
- // so don't short-circuit and check the database for duplicates.
- const timestampIgnoreFuzziness = 5 * 60
- if portal.lastMessageTs > info.Timestamp+timestampIgnoreFuzziness {
- portal.log.Debugfln("Not handling %s (%s): message is >5 minutes older (%d) than last bridge message (%d)", info.Id, msgType, info.Timestamp, portal.lastMessageTs)
- } else if portal.isRecentlyHandled(info.Id) {
- portal.log.Debugfln("Not handling %s (%s): message was recently handled", info.Id, msgType)
- } else if portal.isDuplicate(info.Id) {
- portal.log.Debugfln("Not handling %s (%s): message is duplicate", info.Id, msgType)
- } else {
- portal.lastMessageTs = info.Timestamp
- intent := portal.getMessageIntent(source, info)
- if intent != nil {
- portal.log.Debugfln("Starting handling of %s (%s, ts: %d)", info.Id, msgType, info.Timestamp)
- } else {
- portal.log.Debugfln("Not handling %s (%s): sender is not known", info.Id, msgType)
- }
- return intent
- }
- return nil
-}
-
-func (portal *Portal) finishHandling(source *User, message *waProto.WebMessageInfo, mxid id.EventID) {
- portal.markHandled(source, message, mxid, true)
+func (portal *Portal) finishHandling(existing *database.Message, message *types.MessageInfo, mxid id.EventID, decryptionError bool) {
+ portal.markHandled(existing, message, mxid, true, true, decryptionError)
portal.sendDeliveryReceipt(mxid)
- portal.log.Debugln("Handled message", message.GetKey().GetId(), "->", mxid)
+ if !decryptionError {
+ portal.log.Debugln("Handled message", message.ID, "->", mxid)
+ } else {
+ portal.log.Debugln("Handled message", message.ID, "->", mxid, "(undecryptable message error notice)")
+ }
}
-func (portal *Portal) kickExtraUsers(participantMap map[whatsapp.JID]bool) {
+func (portal *Portal) kickExtraUsers(participantMap map[types.JID]bool) {
members, err := portal.MainIntent().JoinedMembers(portal.MXID)
if err != nil {
portal.log.Warnln("Failed to get member list:", err)
- } else {
- for member := range members.Joined {
- jid, ok := portal.bridge.ParsePuppetMXID(member)
- if ok {
- _, shouldBePresent := participantMap[jid]
- if !shouldBePresent {
- _, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{
- UserID: member,
- Reason: "User had left this WhatsApp chat",
- })
- if err != nil {
- portal.log.Warnfln("Failed to kick user %s who had left: %v", member, err)
- }
+ return
+ }
+ for member := range members.Joined {
+ jid, ok := portal.bridge.ParsePuppetMXID(member)
+ if ok {
+ _, shouldBePresent := participantMap[jid]
+ if !shouldBePresent {
+ _, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{
+ UserID: member,
+ Reason: "User had left this WhatsApp chat",
+ })
+ if err != nil {
+ portal.log.Warnfln("Failed to kick user %s who had left: %v", member, err)
}
}
}
}
}
-func (portal *Portal) SyncBroadcastRecipients(source *User, metadata *whatsapp.BroadcastListInfo) {
- participantMap := make(map[whatsapp.JID]bool)
- for _, recipient := range metadata.Recipients {
- participantMap[recipient.JID] = true
+//func (portal *Portal) SyncBroadcastRecipients(source *User, metadata *whatsapp.BroadcastListInfo) {
+// participantMap := make(map[whatsapp.JID]bool)
+// for _, recipient := range metadata.Recipients {
+// participantMap[recipient.JID] = true
+//
+// puppet := portal.bridge.GetPuppetByJID(recipient.JID)
+// puppet.SyncContactIfNecessary(source)
+// err := puppet.DefaultIntent().EnsureJoined(portal.MXID)
+// if err != nil {
+// portal.log.Warnfln("Failed to make puppet of %s join %s: %v", recipient.JID, portal.MXID, err)
+// }
+// }
+// portal.kickExtraUsers(participantMap)
+//}
- puppet := portal.bridge.GetPuppetByJID(recipient.JID)
- puppet.SyncContactIfNecessary(source)
- err := puppet.DefaultIntent().EnsureJoined(portal.MXID)
- if err != nil {
- portal.log.Warnfln("Failed to make puppet of %s join %s: %v", recipient.JID, portal.MXID, err)
- }
- }
- portal.kickExtraUsers(participantMap)
-}
-
-func (portal *Portal) SyncParticipants(source *User, metadata *whatsapp.GroupInfo) {
+func (portal *Portal) SyncParticipants(source *User, metadata *types.GroupInfo) {
changed := false
levels, err := portal.MainIntent().PowerLevels(portal.MXID)
if err != nil {
levels = portal.GetBasePowerLevels()
changed = true
}
- participantMap := make(map[whatsapp.JID]bool)
+ participantMap := make(map[types.JID]bool)
for _, participant := range metadata.Participants {
participantMap[participant.JID] = true
- user := portal.bridge.GetUserByJID(participant.JID)
- portal.userMXIDAction(user, portal.ensureMXIDInvited)
-
puppet := portal.bridge.GetPuppetByJID(participant.JID)
- puppet.SyncContactIfNecessary(source)
- 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)
+ puppet.SyncContact(source, true)
+ user := portal.bridge.GetUserByJID(participant.JID)
+ if user != nil {
+ portal.ensureUserInvited(user)
+ }
+ if user == nil || !puppet.IntentFor(portal).IsCustomPuppet {
+ 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 {
+ if participant.JID == metadata.OwnerJID {
expectedLevel = 95
} else if participant.IsAdmin {
expectedLevel = 50
@@ -498,71 +552,70 @@ func (portal *Portal) SyncParticipants(source *User, metadata *whatsapp.GroupInf
portal.kickExtraUsers(participantMap)
}
-func (portal *Portal) UpdateAvatar(user *User, avatar *whatsapp.ProfilePicInfo, updateInfo bool) bool {
- if avatar == nil || (avatar.Status == 0 && avatar.Tag != "remove" && len(avatar.URL) == 0) {
- var err error
- avatar, err = user.Conn.GetProfilePicThumb(portal.Key.JID)
- if err != nil {
- portal.log.Errorln(err)
- return false
+func (portal *Portal) UpdateAvatar(user *User, setBy types.JID, updateInfo bool) bool {
+ avatar, err := user.Client.GetProfilePictureInfo(portal.Key.JID, false)
+ if err != nil {
+ if !errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) {
+ portal.log.Warnln("Failed to get avatar URL:", err)
}
- }
-
- if avatar.Status == 404 {
- avatar.Tag = "remove"
- avatar.Status = 0
- }
- if avatar.Status != 0 || portal.Avatar == avatar.Tag {
return false
- }
-
- if avatar.Tag == "remove" {
+ } else if avatar == nil {
+ if portal.Avatar == "remove" {
+ return false
+ }
portal.AvatarURL = id.ContentURI{}
+ avatar = &types.ProfilePictureInfo{ID: "remove"}
+ } else if avatar.ID == portal.Avatar {
+ return false
+ } else if len(avatar.URL) == 0 {
+ portal.log.Warnln("Didn't get URL in response to avatar query")
+ return false
} else {
- data, err := avatar.DownloadBytes()
+ url, err := reuploadAvatar(portal.MainIntent(), avatar.URL)
if err != nil {
- portal.log.Warnln("Failed to download avatar:", err)
+ portal.log.Warnln("Failed to reupload avatar:", err)
return false
}
-
- mimeType := http.DetectContentType(data)
- resp, err := portal.MainIntent().UploadBytes(data, mimeType)
- if err != nil {
- portal.log.Warnln("Failed to upload avatar:", err)
- return false
- }
-
- portal.AvatarURL = resp.ContentURI
+ portal.AvatarURL = url
}
if len(portal.MXID) > 0 {
- _, err := portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL)
+ intent := portal.MainIntent()
+ if !setBy.IsEmpty() {
+ intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
+ }
+ _, err = intent.SetRoomAvatar(portal.MXID, portal.AvatarURL)
+ if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
+ _, err = portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL)
+ }
if err != nil {
portal.log.Warnln("Failed to set room topic:", err)
return false
}
}
- portal.Avatar = avatar.Tag
+ portal.Avatar = avatar.ID
if updateInfo {
portal.UpdateBridgeInfo()
}
return true
}
-func (portal *Portal) UpdateName(name string, setBy whatsapp.JID, intent *appservice.IntentAPI, updateInfo bool) bool {
+func (portal *Portal) UpdateName(name string, setBy types.JID, updateInfo bool) bool {
if name == "" && portal.IsBroadcastList() {
name = UnnamedBroadcastName
}
if portal.Name != name {
portal.log.Debugfln("Updating name %s -> %s", portal.Name, name)
portal.Name = name
- if intent == nil {
- intent = portal.MainIntent()
- if len(setBy) > 0 {
- intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
- }
+
+ intent := portal.MainIntent()
+ if !setBy.IsEmpty() {
+ intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
}
_, err := intent.SetRoomName(portal.MXID, name)
+ if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
+ _, err = portal.MainIntent().SetRoomName(portal.MXID, name)
+ }
if err == nil {
if updateInfo {
portal.UpdateBridgeInfo()
@@ -576,17 +629,19 @@ func (portal *Portal) UpdateName(name string, setBy whatsapp.JID, intent *appser
return false
}
-func (portal *Portal) UpdateTopic(topic string, setBy whatsapp.JID, intent *appservice.IntentAPI, updateInfo bool) bool {
+func (portal *Portal) UpdateTopic(topic string, setBy types.JID, updateInfo bool) bool {
if portal.Topic != topic {
portal.log.Debugfln("Updating topic %s -> %s", portal.Topic, topic)
portal.Topic = topic
- if intent == nil {
- intent = portal.MainIntent()
- if len(setBy) > 0 {
- intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
- }
+
+ intent := portal.MainIntent()
+ if !setBy.IsEmpty() {
+ intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
}
_, err := intent.SetRoomTopic(portal.MXID, topic)
+ if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
+ _, err = portal.MainIntent().SetRoomTopic(portal.MXID, topic)
+ }
if err == nil {
if updateInfo {
portal.UpdateBridgeInfo()
@@ -605,63 +660,41 @@ func (portal *Portal) UpdateMetadata(user *User) bool {
return false
} else if portal.IsStatusBroadcastList() {
update := false
- update = portal.UpdateName(StatusBroadcastName, "", nil, false) || update
- update = portal.UpdateTopic(StatusBroadcastTopic, "", nil, false) || update
+ update = portal.UpdateName(StatusBroadcastName, types.EmptyJID, false) || update
+ update = portal.UpdateTopic(StatusBroadcastTopic, types.EmptyJID, false) || update
return update
} else if portal.IsBroadcastList() {
update := false
- broadcastMetadata, err := user.Conn.GetBroadcastMetadata(portal.Key.JID)
- if err == nil && broadcastMetadata.Status == 200 {
- portal.SyncBroadcastRecipients(user, broadcastMetadata)
- update = portal.UpdateName(broadcastMetadata.Name, "", nil, false) || update
- } else {
- user.Conn.Store.ContactsLock.RLock()
- contact, _ := user.Conn.Store.Contacts[portal.Key.JID]
- user.Conn.Store.ContactsLock.RUnlock()
- update = portal.UpdateName(contact.Name, "", nil, false) || update
- }
- update = portal.UpdateTopic(BroadcastTopic, "", nil, false) || update
+ //broadcastMetadata, err := user.Conn.GetBroadcastMetadata(portal.Key.JID)
+ //if err == nil && broadcastMetadata.Status == 200 {
+ // portal.SyncBroadcastRecipients(user, broadcastMetadata)
+ // update = portal.UpdateName(broadcastMetadata.Name, "", nil, false) || update
+ //} else {
+ // user.Conn.Store.ContactsLock.RLock()
+ // contact, _ := user.Conn.Store.Contacts[portal.Key.JID]
+ // user.Conn.Store.ContactsLock.RUnlock()
+ // update = portal.UpdateName(contact.Name, "", nil, false) || update
+ //}
+ //update = portal.UpdateTopic(BroadcastTopic, "", nil, false) || update
return update
}
- metadata, err := user.Conn.GetGroupMetaData(portal.Key.JID)
+ metadata, err := user.Client.GetGroupInfo(portal.Key.JID)
if err != nil {
- portal.log.Errorln(err)
- return false
- }
- if metadata.Status != 0 {
- // 401: access denied
- // 404: group does (no longer) exist
- // 500: ??? happens with status@broadcast
-
- // TODO: update the room, e.g. change priority level
- // to send messages to moderator
+ portal.log.Errorln("Failed to get group info:", err)
return false
}
portal.SyncParticipants(user, metadata)
update := false
- update = portal.UpdateName(metadata.Name, metadata.NameSetBy, nil, false) || update
- update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, nil, false) || update
+ update = portal.UpdateName(metadata.Name, metadata.NameSetBy, false) || update
+ update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, false) || update
- portal.RestrictMessageSending(metadata.Announce)
+ portal.RestrictMessageSending(metadata.IsAnnounce)
+ portal.RestrictMetadataChanges(metadata.IsLocked)
return update
}
-func (portal *Portal) userMXIDAction(user *User, fn func(mxid id.UserID)) {
- if user == nil {
- return
- }
-
- if user == portal.bridge.Relaybot {
- for _, mxid := range portal.bridge.Config.Bridge.Relaybot.InviteUsers {
- fn(mxid)
- }
- } else {
- fn(user.MXID)
- }
-}
-
func (portal *Portal) ensureMXIDInvited(mxid id.UserID) {
err := portal.MainIntent().EnsureInvited(portal.MXID, mxid)
if err != nil {
@@ -669,16 +702,11 @@ func (portal *Portal) ensureMXIDInvited(mxid id.UserID) {
}
}
-func (portal *Portal) ensureUserInvited(user *User) {
- if user.IsRelaybot {
- portal.userMXIDAction(user, portal.ensureMXIDInvited)
- return
- }
-
+func (portal *Portal) ensureUserInvited(user *User) (ok bool) {
inviteContent := event.Content{
Parsed: &event.MemberEventContent{
Membership: event.MembershipInvite,
- IsDirect: portal.IsPrivateChat(),
+ IsDirect: portal.IsPrivateChat(),
},
Raw: map[string]interface{}{},
}
@@ -690,30 +718,29 @@ func (portal *Portal) ensureUserInvited(user *User) {
var httpErr mautrix.HTTPError
if err != nil && errors.As(err, &httpErr) && httpErr.RespError != nil && strings.Contains(httpErr.RespError.Err, "is already in the room") {
portal.bridge.StateStore.SetMembership(portal.MXID, user.MXID, event.MembershipJoin)
+ ok = true
} else if err != nil {
portal.log.Warnfln("Failed to invite %s: %v", user.MXID, err)
+ } else {
+ ok = true
}
if customPuppet != nil && customPuppet.CustomIntent() != nil {
err = customPuppet.CustomIntent().EnsureJoined(portal.MXID)
if err != nil {
portal.log.Warnfln("Failed to auto-join portal as %s: %v", user.MXID, err)
+ ok = false
+ } else {
+ ok = true
}
}
+ return
}
-func (portal *Portal) Sync(user *User, contact whatsapp.Contact) bool {
+func (portal *Portal) Sync(user *User) bool {
portal.log.Infoln("Syncing portal for", user.MXID)
- if user.IsRelaybot {
- yes := true
- portal.hasRelaybot = &yes
- }
-
if len(portal.MXID) == 0 {
- if !portal.IsPrivateChat() {
- portal.Name = contact.Name
- }
err := portal.CreateMatrixRoom(user)
if err != nil {
portal.log.Errorln("Failed to create portal room:", err)
@@ -726,7 +753,7 @@ func (portal *Portal) Sync(user *User, contact whatsapp.Contact) bool {
update := false
update = portal.UpdateMetadata(user) || update
if !portal.IsPrivateChat() && !portal.IsBroadcastList() && portal.Avatar == "" {
- update = portal.UpdateAvatar(user, nil, false) || update
+ update = portal.UpdateAvatar(user, types.EmptyJID, false) || update
}
if update {
portal.Update()
@@ -760,7 +787,7 @@ func (portal *Portal) GetBasePowerLevels() *event.PowerLevelsEventContent {
}
}
-func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) id.EventID {
+func (portal *Portal) ChangeAdminStatus(jids []types.JID, setAdmin bool) id.EventID {
levels, err := portal.MainIntent().PowerLevels(portal.MXID)
if err != nil {
levels = portal.GetBasePowerLevels()
@@ -839,196 +866,273 @@ func (portal *Portal) RestrictMetadataChanges(restrict bool) id.EventID {
return ""
}
-func (portal *Portal) BackfillHistory(user *User, lastMessageTime int64) error {
- if !portal.bridge.Config.Bridge.RecoverHistory {
+func (portal *Portal) parseWebMessageInfo(webMsg *waProto.WebMessageInfo) *types.MessageInfo {
+ info := types.MessageInfo{
+ MessageSource: types.MessageSource{
+ Chat: portal.Key.JID,
+ IsFromMe: webMsg.GetKey().GetFromMe(),
+ IsGroup: false,
+ },
+ ID: webMsg.GetKey().GetId(),
+ PushName: webMsg.GetPushName(),
+ Timestamp: time.Unix(int64(webMsg.GetMessageTimestamp()), 0),
+ }
+ var err error
+ if info.IsFromMe {
+ info.Sender = portal.Key.Receiver
+ } else if portal.IsPrivateChat() {
+ info.Sender = portal.Key.JID
+ } else if webMsg.GetParticipant() != "" {
+ info.Sender, err = types.ParseJID(webMsg.GetParticipant())
+ } else if webMsg.GetKey().GetParticipant() != "" {
+ info.Sender, err = types.ParseJID(webMsg.GetKey().GetParticipant())
+ }
+ if info.Sender.IsEmpty() {
+ portal.log.Warnfln("Failed to get sender of message %s (parse error: %v)", info.ID, err)
return nil
}
+ return &info
+}
- endBackfill := portal.beginBackfill()
- defer endBackfill()
+const backfillIDField = "net.maunium.whatsapp.id"
- lastMessage := portal.bridge.DB.Message.GetLastInChat(portal.Key)
- if lastMessage == nil {
- return nil
+func (portal *Portal) wrapBatchEvent(info *types.MessageInfo, intent *appservice.IntentAPI, eventType event.Type, content *event.MessageEventContent) (*event.Event, error) {
+ wrappedContent := event.Content{
+ Parsed: content,
+ Raw: map[string]interface{}{
+ backfillIDField: info.ID,
+ },
}
- if lastMessage.Timestamp >= lastMessageTime {
- portal.log.Debugln("Not backfilling: no new messages")
- return nil
+ if intent.IsCustomPuppet {
+ wrappedContent.Raw["net.maunium.whatsapp.puppet"] = intent.IsCustomPuppet
}
+ newEventType, err := portal.encrypt(&wrappedContent, eventType)
+ if err != nil {
+ return nil, err
+ }
+ return &event.Event{
+ Sender: intent.UserID,
+ Type: newEventType,
+ Timestamp: info.Timestamp.UnixMilli(),
+ Content: wrappedContent,
+ }, nil
+}
- lastMessageID := lastMessage.JID
- lastMessageFromMe := lastMessage.Sender == user.JID
- portal.log.Infoln("Backfilling history since", lastMessageID, "for", user.MXID)
- for len(lastMessageID) > 0 {
- portal.log.Debugln("Fetching 50 messages of history after", lastMessageID)
- resp, err := user.Conn.LoadMessagesAfter(portal.Key.JID, lastMessageID, lastMessageFromMe, 50)
- if err == whatsapp.ErrServerRespondedWith404 {
- portal.log.Warnln("Got 404 response trying to fetch messages to backfill. Fetching latest messages as fallback.")
- resp, err = user.Conn.LoadMessagesBefore(portal.Key.JID, "", true, 50)
- }
+func (portal *Portal) appendBatchEvents(converted *ConvertedMessage, info *types.MessageInfo, eventsArray *[]*event.Event, infoArray *[]*types.MessageInfo) error {
+ mainEvt, err := portal.wrapBatchEvent(info, converted.Intent, converted.Type, converted.Content)
+ if err != nil {
+ return err
+ }
+ if converted.Caption != nil {
+ captionEvt, err := portal.wrapBatchEvent(info, converted.Intent, converted.Type, converted.Caption)
if err != nil {
return err
}
- messages, ok := resp.Content.([]interface{})
- if !ok || len(messages) == 0 {
- portal.log.Debugfln("Didn't get more messages to backfill (resp.Content is %T)", resp.Content)
- break
- }
-
- portal.handleHistory(user, messages)
-
- lastMessageProto, ok := messages[len(messages)-1].(*waProto.WebMessageInfo)
- if ok {
- lastMessageID = lastMessageProto.GetKey().GetId()
- lastMessageFromMe = lastMessageProto.GetKey().GetFromMe()
- }
+ *eventsArray = append(*eventsArray, mainEvt, captionEvt)
+ *infoArray = append(*infoArray, nil, info)
+ } else {
+ *eventsArray = append(*eventsArray, mainEvt)
+ *infoArray = append(*infoArray, info)
}
- portal.log.Infoln("Backfilling finished")
return nil
}
-func (portal *Portal) beginBackfill() func() {
+func (portal *Portal) finishBatch(eventIDs []id.EventID, infos []*types.MessageInfo) {
+ if len(eventIDs) != len(infos) {
+ portal.log.Errorfln("Length of event IDs (%d) and message infos (%d) doesn't match! Using slow path for mapping event IDs", len(eventIDs), len(infos))
+ infoMap := make(map[types.MessageID]*types.MessageInfo, len(infos))
+ for _, info := range infos {
+ infoMap[info.ID] = info
+ }
+ for _, eventID := range eventIDs {
+ if evt, err := portal.MainIntent().GetEvent(portal.MXID, eventID); err != nil {
+ portal.log.Warnfln("Failed to get event %s to register it in the database: %v", eventID, err)
+ } else if msgID, ok := evt.Content.Raw[backfillIDField].(string); !ok {
+ portal.log.Warnfln("Event %s doesn't include the WhatsApp message ID", eventID)
+ } else if info, ok := infoMap[types.MessageID(msgID)]; !ok {
+ portal.log.Warnfln("Didn't find info of message %s (event %s) to register it in the database", msgID, eventID)
+ } else {
+ portal.markHandled(nil, info, eventID, true, false, false)
+ }
+ }
+ } else {
+ for i := 0; i < len(infos); i++ {
+ if infos[i] != nil {
+ portal.markHandled(nil, infos[i], eventIDs[i], true, false, false)
+ }
+ }
+ portal.log.Infofln("Successfully sent %d events", len(eventIDs))
+ }
+}
+
+func (portal *Portal) backfill(source *User, messages []*waProto.HistorySyncMsg) {
portal.backfillLock.Lock()
- portal.backfilling = true
- var privateChatPuppetInvited bool
- var privateChatPuppet *Puppet
- if portal.IsPrivateChat() && portal.bridge.Config.Bridge.InviteOwnPuppetForBackfilling && portal.Key.JID != portal.Key.Receiver {
- privateChatPuppet = portal.bridge.GetPuppetByJID(portal.Key.Receiver)
- portal.privateChatBackfillInvitePuppet = func() {
- if privateChatPuppetInvited {
- return
+ defer portal.backfillLock.Unlock()
+
+ var historyBatch, newBatch mautrix.ReqBatchSend
+ var historyBatchInfos, newBatchInfos []*types.MessageInfo
+
+ firstMsgTimestamp := time.Unix(int64(messages[len(messages)-1].GetMessage().GetMessageTimestamp()), 0)
+
+ historyBatch.StateEventsAtStart = make([]*event.Event, 1)
+ newBatch.StateEventsAtStart = make([]*event.Event, 1)
+
+ // TODO remove the dummy state events after https://github.com/matrix-org/synapse/pull/11188
+ emptyStr := ""
+ dummyStateEvent := event.Event{
+ Type: BackfillDummyStateEvent,
+ Sender: portal.MainIntent().UserID,
+ StateKey: &emptyStr,
+ Timestamp: firstMsgTimestamp.UnixMilli(),
+ Content: event.Content{},
+ }
+ historyBatch.StateEventsAtStart[0] = &dummyStateEvent
+ newBatch.StateEventsAtStart[0] = &dummyStateEvent
+
+ addedMembers := make(map[id.UserID]*event.MemberEventContent)
+ addMember := func(puppet *Puppet) {
+ if _, alreadyAdded := addedMembers[puppet.MXID]; alreadyAdded {
+ return
+ }
+ mxid := puppet.MXID.String()
+ content := event.MemberEventContent{
+ Membership: event.MembershipJoin,
+ Displayname: puppet.Displayname,
+ AvatarURL: puppet.AvatarURL.CUString(),
+ }
+ inviteContent := content
+ inviteContent.Membership = event.MembershipInvite
+ historyBatch.StateEventsAtStart = append(historyBatch.StateEventsAtStart, &event.Event{
+ Type: event.StateMember,
+ Sender: portal.MainIntent().UserID,
+ StateKey: &mxid,
+ Timestamp: firstMsgTimestamp.UnixMilli(),
+ Content: event.Content{Parsed: &inviteContent},
+ }, &event.Event{
+ Type: event.StateMember,
+ Sender: puppet.MXID,
+ StateKey: &mxid,
+ Timestamp: firstMsgTimestamp.UnixMilli(),
+ Content: event.Content{Parsed: &content},
+ })
+ addedMembers[puppet.MXID] = &content
+ }
+
+ firstMessage := portal.bridge.DB.Message.GetFirstInChat(portal.Key)
+ lastMessage := portal.bridge.DB.Message.GetLastInChat(portal.Key)
+ var historyMaxTs, newMinTs time.Time
+
+ if portal.FirstEventID != "" || portal.NextBatchID != "" {
+ historyBatch.PrevEventID = portal.FirstEventID
+ historyBatch.BatchID = portal.NextBatchID
+ if firstMessage == nil && lastMessage == nil {
+ historyMaxTs = time.Now()
+ } else {
+ historyMaxTs = firstMessage.Timestamp
+ }
+ }
+ if lastMessage != nil {
+ newBatch.PrevEventID = lastMessage.MXID
+ newMinTs = lastMessage.Timestamp
+ }
+
+ portal.log.Infofln("Processing history sync with %d messages", len(messages))
+ // The messages are ordered newest to oldest, so iterate them in reverse order.
+ for i := len(messages) - 1; i >= 0; i-- {
+ wrappedMsg := messages[i]
+ webMsg := wrappedMsg.GetMessage()
+ msgType := portal.getMessageType(webMsg.GetMessage())
+ if msgType == "unknown" || msgType == "ignore" {
+ if msgType == "unknown" {
+ portal.log.Debugfln("Skipping message %s with unknown type in backfill", webMsg.GetKey().GetId())
}
- privateChatPuppetInvited = true
- _, _ = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: privateChatPuppet.MXID})
- _ = privateChatPuppet.DefaultIntent().EnsureJoined(portal.MXID)
+ continue
}
- }
- return func() {
- portal.backfilling = false
- portal.privateChatBackfillInvitePuppet = nil
- portal.backfillLock.Unlock()
- if privateChatPuppet != nil && privateChatPuppetInvited {
- _, _ = privateChatPuppet.DefaultIntent().LeaveRoom(portal.MXID)
+ info := portal.parseWebMessageInfo(webMsg)
+ if info == nil {
+ continue
}
- }
-}
-
-func (portal *Portal) disableNotifications(user *User) {
- if !portal.bridge.Config.Bridge.HistoryDisableNotifs {
- return
- }
- puppet := portal.bridge.GetPuppetByCustomMXID(user.MXID)
- if puppet == nil || puppet.customIntent == nil {
- return
- }
- portal.log.Debugfln("Disabling notifications for %s for backfilling", user.MXID)
- ruleID := fmt.Sprintf("net.maunium.silence_while_backfilling.%s", portal.MXID)
- err := puppet.customIntent.PutPushRule("global", pushrules.OverrideRule, ruleID, &mautrix.ReqPutPushRule{
- Actions: []pushrules.PushActionType{pushrules.ActionDontNotify},
- Conditions: []pushrules.PushCondition{{
- Kind: pushrules.KindEventMatch,
- Key: "room_id",
- Pattern: string(portal.MXID),
- }},
- })
- if err != nil {
- portal.log.Warnfln("Failed to disable notifications for %s while backfilling: %v", user.MXID, err)
- }
-}
-
-func (portal *Portal) enableNotifications(user *User) {
- if !portal.bridge.Config.Bridge.HistoryDisableNotifs {
- return
- }
- puppet := portal.bridge.GetPuppetByCustomMXID(user.MXID)
- if puppet == nil || puppet.customIntent == nil {
- return
- }
- portal.log.Debugfln("Re-enabling notifications for %s after backfilling", user.MXID)
- ruleID := fmt.Sprintf("net.maunium.silence_while_backfilling.%s", portal.MXID)
- err := puppet.customIntent.DeletePushRule("global", pushrules.OverrideRule, ruleID)
- if err != nil {
- portal.log.Warnfln("Failed to re-enable notifications for %s after backfilling: %v", user.MXID, err)
- }
-}
-
-func (portal *Portal) FillInitialHistory(user *User) error {
- if portal.bridge.Config.Bridge.InitialHistoryFill == 0 {
- return nil
- }
- endBackfill := portal.beginBackfill()
- defer endBackfill()
- if portal.privateChatBackfillInvitePuppet != nil {
- portal.privateChatBackfillInvitePuppet()
- }
-
- n := portal.bridge.Config.Bridge.InitialHistoryFill
- portal.log.Infoln("Filling initial history, maximum", n, "messages")
- var messages []interface{}
- before := ""
- fromMe := true
- chunkNum := 0
- for n > 0 {
- chunkNum += 1
- count := 50
- if n < count {
- count = n
+ var batch *mautrix.ReqBatchSend
+ var infos *[]*types.MessageInfo
+ var history bool
+ if !historyMaxTs.IsZero() && info.Timestamp.Before(historyMaxTs) {
+ batch, infos, history = &historyBatch, &historyBatchInfos, true
+ } else if !newMinTs.IsZero() && info.Timestamp.After(newMinTs) {
+ batch, infos = &newBatch, &newBatchInfos
+ } else {
+ continue
}
- portal.log.Debugfln("Fetching chunk %d (%d messages / %d cap) before message %s", chunkNum, count, n, before)
- resp, err := user.Conn.LoadMessagesBefore(portal.Key.JID, before, fromMe, count)
+ puppet := portal.getMessagePuppet(source, info)
+ var intent *appservice.IntentAPI
+ if portal.Key.JID == puppet.JID {
+ intent = puppet.DefaultIntent()
+ } else {
+ intent = puppet.IntentFor(portal)
+ if intent.IsCustomPuppet && !portal.bridge.Config.Bridge.HistorySync.DoublePuppetBackfill {
+ intent = puppet.DefaultIntent()
+ addMember(puppet)
+ }
+ }
+ converted := portal.convertMessage(intent, source, info, webMsg.GetMessage())
+ if converted == nil {
+ portal.log.Debugfln("Skipping unsupported message %s in backfill", info.ID)
+ continue
+ }
+ if history && !portal.IsPrivateChat() && !portal.bridge.StateStore.IsInRoom(portal.MXID, puppet.MXID) {
+ addMember(puppet)
+ }
+ err := portal.appendBatchEvents(converted, info, &batch.Events, infos)
if err != nil {
- return err
- }
- chunk, ok := resp.Content.([]interface{})
- if !ok || len(chunk) == 0 {
- portal.log.Infoln("Chunk empty, starting handling of loaded messages")
- break
- }
-
- messages = append(chunk, messages...)
-
- portal.log.Debugfln("Fetched chunk and received %d messages", len(chunk))
-
- n -= len(chunk)
- key := chunk[0].(*waProto.WebMessageInfo).GetKey()
- before = key.GetId()
- fromMe = key.GetFromMe()
- if len(before) == 0 {
- portal.log.Infoln("No message ID for first message, starting handling of loaded messages")
- break
+ portal.log.Errorfln("Error handling message %s during backfill: %v", info.ID, err)
+ }
+ }
+
+ if len(historyBatch.Events) > 0 && len(historyBatch.PrevEventID) > 0 {
+ portal.log.Infofln("Sending %d historical messages...", len(historyBatch.Events))
+ historyResp, err := portal.MainIntent().BatchSend(portal.MXID, &historyBatch)
+ if err != nil {
+ portal.log.Errorln("Error sending batch of historical messages:", err)
+ } else {
+ portal.finishBatch(historyResp.EventIDs, historyBatchInfos)
+ portal.NextBatchID = historyResp.NextBatchID
+ portal.Update()
+ // If batchID is non-empty, it means this is backfilling very old messages, and we don't need a post-backfill dummy.
+ if historyBatch.BatchID == "" {
+ portal.sendPostBackfillDummy(time.UnixMilli(historyBatch.Events[len(historyBatch.Events)-1].Timestamp))
+ }
+ }
+ }
+
+ if len(newBatch.Events) > 0 && len(newBatch.PrevEventID) > 0 {
+ portal.log.Debugln("Sending a dummy event to avoid forward extremity errors with forward backfill")
+ _, err := portal.MainIntent().SendMessageEvent(portal.MXID, ForwardBackfillDummyEvent, struct{}{})
+ if err != nil {
+ portal.log.Warnln("Error sending dummy event for forward backfill:", err)
+ }
+ portal.log.Infofln("Sending %d new messages...", len(newBatch.Events))
+ newResp, err := portal.MainIntent().BatchSend(portal.MXID, &newBatch)
+ if err != nil {
+ portal.log.Errorln("Error sending batch of new messages:", err)
+ } else {
+ portal.finishBatch(newResp.EventIDs, newBatchInfos)
+ portal.sendPostBackfillDummy(time.UnixMilli(newBatch.Events[len(newBatch.Events)-1].Timestamp))
}
}
- portal.disableNotifications(user)
- portal.handleHistory(user, messages)
- portal.enableNotifications(user)
- portal.log.Infoln("Initial history fill complete")
- return nil
}
-func (portal *Portal) handleHistory(user *User, messages []interface{}) {
- portal.log.Infoln("Handling", len(messages), "messages of history")
- for _, rawMessage := range messages {
- message, ok := rawMessage.(*waProto.WebMessageInfo)
- if !ok {
- portal.log.Warnln("Unexpected non-WebMessageInfo item in history response:", rawMessage)
- continue
- }
- data := whatsapp.ParseProtoMessage(message)
- if data == nil || data == whatsapp.ErrMessageTypeNotImplemented {
- st := message.GetMessageStubType()
- // Ignore some types that are known to fail
- if st == waProto.WebMessageInfo_CALL_MISSED_VOICE || st == waProto.WebMessageInfo_CALL_MISSED_VIDEO ||
- st == waProto.WebMessageInfo_CALL_MISSED_GROUP_VOICE || st == waProto.WebMessageInfo_CALL_MISSED_GROUP_VIDEO {
- continue
- }
- portal.log.Warnln("Message", message.GetKey().GetId(), "failed to parse during backfilling")
- continue
- }
- if portal.privateChatBackfillInvitePuppet != nil && message.GetKey().GetFromMe() && portal.IsPrivateChat() {
- portal.privateChatBackfillInvitePuppet()
- }
- portal.handleMessage(PortalMessage{portal.Key.JID, user, data, message.GetMessageTimestamp()}, true)
+func (portal *Portal) sendPostBackfillDummy(lastTimestamp time.Time) {
+ resp, err := portal.MainIntent().SendMessageEvent(portal.MXID, BackfillEndDummyEvent, struct{}{})
+ if err != nil {
+ portal.log.Errorln("Error sending post-backfill dummy event:", err)
+ return
}
+ msg := portal.bridge.DB.Message.New()
+ msg.Chat = portal.Key
+ msg.MXID = resp.EventID
+ msg.JID = types.MessageID(resp.EventID)
+ msg.Timestamp = lastTimestamp.Add(1 * time.Second)
+ msg.Sent = true
+ msg.Insert()
}
type BridgeInfoSection struct {
@@ -1062,7 +1166,7 @@ func (portal *Portal) getBridgeInfo() (string, BridgeInfoContent) {
ExternalURL: "https://www.whatsapp.com/",
},
Channel: BridgeInfoSection{
- ID: portal.Key.JID,
+ ID: portal.Key.JID.String(),
DisplayName: portal.Name,
AvatarURL: portal.AvatarURL.CUString(),
},
@@ -1088,6 +1192,11 @@ func (portal *Portal) UpdateBridgeInfo() {
}
}
+var PortalCreationDummyEvent = event.Type{Type: "fi.mau.dummy.portal_created", Class: event.MessageEventType}
+var BackfillDummyStateEvent = event.Type{Type: "fi.mau.dummy.blank_backfill_state", Class: event.StateEventType}
+var BackfillEndDummyEvent = event.Type{Type: "fi.mau.dummy.backfill_end", Class: event.MessageEventType}
+var ForwardBackfillDummyEvent = event.Type{Type: "fi.mau.dummy.pre_forward_backfill", Class: event.MessageEventType}
+
func (portal *Portal) CreateMatrixRoom(user *User) error {
portal.roomCreateLock.Lock()
defer portal.roomCreateLock.Unlock()
@@ -1102,11 +1211,11 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
portal.log.Infoln("Creating Matrix room. Info source:", user.MXID)
- var metadata *whatsapp.GroupInfo
- var broadcastMetadata *whatsapp.BroadcastListInfo
+ var metadata *types.GroupInfo
+ //var broadcastMetadata *types.BroadcastListInfo
if portal.IsPrivateChat() {
puppet := portal.bridge.GetPuppetByJID(portal.Key.JID)
- puppet.SyncContactIfNecessary(user)
+ puppet.SyncContact(user, true)
if portal.bridge.Config.Bridge.PrivateChatPortalMeta {
portal.Name = puppet.Displayname
portal.AvatarURL = puppet.AvatarURL
@@ -1123,28 +1232,30 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
portal.Name = StatusBroadcastName
portal.Topic = StatusBroadcastTopic
} else if portal.IsBroadcastList() {
- var err error
- broadcastMetadata, err = user.Conn.GetBroadcastMetadata(portal.Key.JID)
- if err == nil && broadcastMetadata.Status == 200 {
- portal.Name = broadcastMetadata.Name
- } else {
- user.Conn.Store.ContactsLock.RLock()
- contact, _ := user.Conn.Store.Contacts[portal.Key.JID]
- user.Conn.Store.ContactsLock.RUnlock()
- portal.Name = contact.Name
- }
- if len(portal.Name) == 0 {
- portal.Name = UnnamedBroadcastName
- }
- portal.Topic = BroadcastTopic
+ //var err error
+ //broadcastMetadata, err = user.Conn.GetBroadcastMetadata(portal.Key.JID)
+ //if err == nil && broadcastMetadata.Status == 200 {
+ // portal.Name = broadcastMetadata.Name
+ //} else {
+ // user.Conn.Store.ContactsLock.RLock()
+ // contact, _ := user.Conn.Store.Contacts[portal.Key.JID]
+ // user.Conn.Store.ContactsLock.RUnlock()
+ // portal.Name = contact.Name
+ //}
+ //if len(portal.Name) == 0 {
+ // portal.Name = UnnamedBroadcastName
+ //}
+ //portal.Topic = BroadcastTopic
+ portal.log.Debugln("Broadcast list is not yet supported, not creating room after all")
+ return fmt.Errorf("broadcast list bridging is currently not supported")
} else {
var err error
- metadata, err = user.Conn.GetGroupMetaData(portal.Key.JID)
- if err == nil && metadata.Status == 0 {
+ metadata, err = user.Client.GetGroupInfo(portal.Key.JID)
+ if err == nil {
portal.Name = metadata.Name
portal.Topic = metadata.Topic
}
- portal.UpdateAvatar(user, nil, false)
+ portal.UpdateAvatar(user, types.EmptyJID, false)
}
bridgeInfoStateKey, bridgeInfo := portal.getBridgeInfo()
@@ -1174,9 +1285,6 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
}
var invite []id.UserID
- if user.IsRelaybot {
- invite = portal.bridge.Config.Bridge.Relaybot.InviteUsers
- }
if portal.bridge.Config.Bridge.Encryption.Default {
initialState = append(initialState, &event.Event{
@@ -1215,20 +1323,22 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
}
portal.ensureUserInvited(user)
+ user.syncChatDoublePuppetDetails(portal, true)
if metadata != nil {
portal.SyncParticipants(user, metadata)
- if metadata.Announce {
- portal.RestrictMessageSending(metadata.Announce)
+ if metadata.IsAnnounce {
+ portal.RestrictMessageSending(metadata.IsAnnounce)
+ }
+ if metadata.IsLocked {
+ portal.RestrictMetadataChanges(metadata.IsLocked)
}
}
- if broadcastMetadata != nil {
- portal.SyncBroadcastRecipients(user, broadcastMetadata)
- }
- inCommunity := user.addPortalToCommunity(portal)
- if portal.IsPrivateChat() && !user.IsRelaybot {
+ //if broadcastMetadata != nil {
+ // portal.SyncBroadcastRecipients(user, broadcastMetadata)
+ //}
+ if portal.IsPrivateChat() {
puppet := user.bridge.GetPuppetByJID(portal.Key.JID)
- user.addPuppetToCommunity(puppet)
if portal.bridge.Config.Bridge.Encryption.Default {
err = portal.bridge.Bot.EnsureJoined(portal.MXID)
@@ -1240,43 +1350,39 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
user.UpdateDirectChats(map[id.UserID][]id.RoomID{puppet.MXID: {portal.MXID}})
}
- user.CreateUserPortal(database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity})
-
- err = portal.FillInitialHistory(user)
+ firstEventResp, err := portal.MainIntent().SendMessageEvent(portal.MXID, PortalCreationDummyEvent, struct{}{})
if err != nil {
- portal.log.Errorln("Failed to fill history:", err)
+ portal.log.Errorln("Failed to send dummy event to mark portal creation:", err)
+ } else {
+ portal.FirstEventID = firstEventResp.EventID
+ portal.Update()
}
return nil
}
func (portal *Portal) IsPrivateChat() bool {
- if portal.isPrivate == nil {
- val := strings.HasSuffix(portal.Key.JID, whatsapp.NewUserSuffix)
- portal.isPrivate = &val
- }
- return *portal.isPrivate
+ return portal.Key.JID.Server == types.DefaultUserServer
}
func (portal *Portal) IsBroadcastList() bool {
- if portal.isBroadcast == nil {
- val := strings.HasSuffix(portal.Key.JID, whatsapp.BroadcastSuffix)
- portal.isBroadcast = &val
- }
- return *portal.isBroadcast
+ return portal.Key.JID.Server == types.BroadcastServer
}
func (portal *Portal) IsStatusBroadcastList() bool {
- return portal.Key.JID == "status@broadcast"
+ return portal.Key.JID == types.StatusBroadcastJID
}
func (portal *Portal) HasRelaybot() bool {
- if portal.bridge.Relaybot == nil {
- return false
- } else if portal.hasRelaybot == nil {
- val := portal.bridge.Relaybot.IsInPortal(portal.Key)
- portal.hasRelaybot = &val
+ return portal.bridge.Config.Bridge.Relay.Enabled && len(portal.RelayUserID) > 0
+}
+
+func (portal *Portal) GetRelayUser() *User {
+ if !portal.HasRelaybot() {
+ return nil
+ } else if portal.relayUser == nil {
+ portal.relayUser = portal.bridge.GetUserByMXID(portal.RelayUserID)
}
- return *portal.hasRelaybot
+ return portal.relayUser
}
func (portal *Portal) MainIntent() *appservice.IntentAPI {
@@ -1286,19 +1392,19 @@ func (portal *Portal) MainIntent() *appservice.IntentAPI {
return portal.bridge.Bot
}
-func (portal *Portal) SetReply(content *event.MessageEventContent, info whatsapp.ContextInfo) {
- if len(info.QuotedMessageID) == 0 {
+func (portal *Portal) SetReply(content *event.MessageEventContent, replyToID types.MessageID) {
+ if len(replyToID) == 0 {
return
}
- message := portal.bridge.DB.Message.GetByJID(portal.Key, info.QuotedMessageID)
+ message := portal.bridge.DB.Message.GetByJID(portal.Key, replyToID)
if message != nil && !message.IsFakeMXID() {
evt, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID)
if err != nil {
portal.log.Warnln("Failed to get reply target:", err)
return
}
+ _ = evt.Content.ParseRaw(evt.Type)
if evt.Type == event.EventEncrypted {
- _ = evt.Content.ParseRaw(evt.Type)
decryptedEvt, err := portal.bridge.Crypto.Decrypt(evt)
if err != nil {
portal.log.Warnln("Failed to decrypt reply target:", err)
@@ -1306,69 +1412,76 @@ func (portal *Portal) SetReply(content *event.MessageEventContent, info whatsapp
evt = decryptedEvt
}
}
- _ = evt.Content.ParseRaw(evt.Type)
content.SetReply(evt)
}
return
}
-func (portal *Portal) HandleMessageRevoke(user *User, message whatsapp.MessageRevocation) bool {
- msg := portal.bridge.DB.Message.GetByJID(portal.Key, message.Id)
+func (portal *Portal) HandleMessageRevoke(user *User, info *types.MessageInfo, key *waProto.MessageKey) bool {
+ msg := portal.bridge.DB.Message.GetByJID(portal.Key, key.GetId())
if msg == nil || msg.IsFakeMXID() {
return false
}
- var intent *appservice.IntentAPI
- if message.FromMe {
- if portal.IsPrivateChat() {
- intent = portal.bridge.GetPuppetByJID(user.JID).CustomIntent()
- } else {
- intent = portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal)
- }
- } else if len(message.Participant) > 0 {
- intent = portal.bridge.GetPuppetByJID(message.Participant).IntentFor(portal)
- }
- if intent == nil {
- intent = portal.MainIntent()
- }
+ intent := portal.bridge.GetPuppetByJID(info.Sender).IntentFor(portal)
_, err := intent.RedactEvent(portal.MXID, msg.MXID)
if err != nil {
- portal.log.Errorln("Failed to redact %s: %v", msg.JID, err)
+ if errors.Is(err, mautrix.MForbidden) {
+ _, err = portal.MainIntent().RedactEvent(portal.MXID, msg.MXID)
+ if err != nil {
+ portal.log.Errorln("Failed to redact %s: %v", msg.JID, err)
+ }
+ }
} else {
msg.Delete()
}
return true
}
-func (portal *Portal) HandleFakeMessage(_ *User, message FakeMessage) bool {
- if portal.isRecentlyHandled(message.ID) {
- return false
- }
-
- content := event.MessageEventContent{
- MsgType: event.MsgNotice,
- Body: message.Text,
- }
- if message.Alert {
- content.MsgType = event.MsgText
- }
- _, err := portal.sendMainIntentMessage(content)
- if err != nil {
- portal.log.Errorfln("Failed to handle fake message %s: %v", message.ID, err)
- return true
- }
-
- portal.recentlyHandledLock.Lock()
- index := portal.recentlyHandledIndex
- portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength
- portal.recentlyHandledLock.Unlock()
- portal.recentlyHandled[index] = message.ID
- return true
-}
+//func (portal *Portal) HandleFakeMessage(_ *User, message FakeMessage) bool {
+// if portal.isRecentlyHandled(message.ID) {
+// return false
+// }
+//
+// content := event.MessageEventContent{
+// MsgType: event.MsgNotice,
+// Body: message.Text,
+// }
+// if message.Alert {
+// content.MsgType = event.MsgText
+// }
+// _, err := portal.sendMainIntentMessage(content)
+// if err != nil {
+// portal.log.Errorfln("Failed to handle fake message %s: %v", message.ID, err)
+// return true
+// }
+//
+// portal.recentlyHandledLock.Lock()
+// index := portal.recentlyHandledIndex
+// portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength
+// portal.recentlyHandledLock.Unlock()
+// portal.recentlyHandled[index] = message.ID
+// return true
+//}
func (portal *Portal) sendMainIntentMessage(content interface{}) (*mautrix.RespSendEvent, error) {
return portal.sendMessage(portal.MainIntent(), event.EventMessage, content, 0)
}
+func (portal *Portal) encrypt(content *event.Content, eventType event.Type) (event.Type, error) {
+ if portal.Encrypted && portal.bridge.Crypto != nil {
+ // TODO maybe the locking should be inside mautrix-go?
+ portal.encryptLock.Lock()
+ encrypted, err := portal.bridge.Crypto.Encrypt(portal.MXID, eventType, *content)
+ portal.encryptLock.Unlock()
+ if err != nil {
+ return eventType, fmt.Errorf("failed to encrypt event: %w", err)
+ }
+ eventType = event.EventEncrypted
+ content.Parsed = encrypted
+ }
+ return eventType, nil
+}
+
func (portal *Portal) sendMessage(intent *appservice.IntentAPI, eventType event.Type, content interface{}, timestamp int64) (*mautrix.RespSendEvent, error) {
wrappedContent := event.Content{Parsed: content}
if timestamp != 0 && intent.IsCustomPuppet {
@@ -1376,16 +1489,10 @@ func (portal *Portal) sendMessage(intent *appservice.IntentAPI, eventType event.
"net.maunium.whatsapp.puppet": intent.IsCustomPuppet,
}
}
- if portal.Encrypted && portal.bridge.Crypto != nil {
- // TODO maybe the locking should be inside mautrix-go?
- portal.encryptLock.Lock()
- encrypted, err := portal.bridge.Crypto.Encrypt(portal.MXID, eventType, wrappedContent)
- portal.encryptLock.Unlock()
- if err != nil {
- return nil, fmt.Errorf("failed to encrypt event: %w", err)
- }
- eventType = event.EventEncrypted
- wrappedContent.Parsed = encrypted
+ var err error
+ eventType, err = portal.encrypt(&wrappedContent, eventType)
+ if err != nil {
+ return nil, err
}
_, _ = intent.UserTyping(portal.MXID, false, 0)
if timestamp == 0 {
@@ -1395,119 +1502,116 @@ func (portal *Portal) sendMessage(intent *appservice.IntentAPI, eventType event.
}
}
-func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessage) bool {
- intent := portal.startHandling(source, message.Info, "text")
- if intent == nil {
- return false
- }
+type ConvertedMessage struct {
+ Intent *appservice.IntentAPI
+ Type event.Type
+ Content *event.MessageEventContent
+ Caption *event.MessageEventContent
+}
+func (portal *Portal) convertTextMessage(intent *appservice.IntentAPI, msg *waProto.Message) *ConvertedMessage {
content := &event.MessageEventContent{
- Body: message.Text,
+ Body: msg.GetConversation(),
MsgType: event.MsgText,
}
+ if msg.GetExtendedTextMessage() != nil {
+ content.Body = msg.GetExtendedTextMessage().GetText()
- portal.bridge.Formatter.ParseWhatsApp(content, message.ContextInfo.MentionedJID)
- portal.SetReply(content, message.ContextInfo)
-
- resp, err := portal.sendMessage(intent, event.EventMessage, content, int64(message.Info.Timestamp*1000))
- if err != nil {
- portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
- } else {
- portal.finishHandling(source, message.Info.Source, resp.EventID)
- }
- return true
-}
-
-func (portal *Portal) HandleStubMessage(source *User, message whatsapp.StubMessage, isBackfill bool) bool {
- if portal.bridge.Config.Bridge.ChatMetaSync && (!portal.IsBroadcastList() || isBackfill) {
- // Chat meta sync is enabled, so we use chat update commands and full-syncs instead of message history
- // However, broadcast lists don't have update commands, so we handle these if it's not a backfill
- return false
- }
- intent := portal.startHandling(source, message.Info, fmt.Sprintf("stub %s", message.Type.String()))
- if intent == nil {
- return false
- }
- var senderJID string
- if message.Info.FromMe {
- senderJID = source.JID
- } else {
- senderJID = message.Info.SenderJid
- }
- var eventID id.EventID
- // TODO find more real event IDs
- // TODO timestamp massaging
- switch message.Type {
- case waProto.WebMessageInfo_GROUP_CHANGE_SUBJECT:
- portal.UpdateName(message.FirstParam, "", intent, true)
- case waProto.WebMessageInfo_GROUP_CHANGE_ICON:
- portal.UpdateAvatar(source, nil, true)
- case waProto.WebMessageInfo_GROUP_CHANGE_DESCRIPTION:
- if isBackfill {
- // TODO fetch topic from server
+ contextInfo := msg.GetExtendedTextMessage().GetContextInfo()
+ if contextInfo != nil {
+ portal.bridge.Formatter.ParseWhatsApp(content, contextInfo.GetMentionedJid())
+ portal.SetReply(content, contextInfo.GetStanzaId())
}
- //portal.UpdateTopic(message.FirstParam, "", intent, true)
- case waProto.WebMessageInfo_GROUP_CHANGE_ANNOUNCE:
- eventID = portal.RestrictMessageSending(message.FirstParam == "on")
- case waProto.WebMessageInfo_GROUP_CHANGE_RESTRICT:
- eventID = portal.RestrictMetadataChanges(message.FirstParam == "on")
- case waProto.WebMessageInfo_GROUP_PARTICIPANT_ADD, waProto.WebMessageInfo_GROUP_PARTICIPANT_INVITE, waProto.WebMessageInfo_BROADCAST_ADD:
- eventID = portal.HandleWhatsAppInvite(source, senderJID, intent, message.Params)
- case waProto.WebMessageInfo_GROUP_PARTICIPANT_REMOVE, waProto.WebMessageInfo_GROUP_PARTICIPANT_LEAVE, waProto.WebMessageInfo_BROADCAST_REMOVE:
- portal.HandleWhatsAppKick(source, senderJID, message.Params)
- case waProto.WebMessageInfo_GROUP_PARTICIPANT_PROMOTE:
- eventID = portal.ChangeAdminStatus(message.Params, true)
- case waProto.WebMessageInfo_GROUP_PARTICIPANT_DEMOTE:
- eventID = portal.ChangeAdminStatus(message.Params, false)
- default:
- return false
}
- if len(eventID) == 0 {
- eventID = id.EventID(fmt.Sprintf("net.maunium.whatsapp.fake::%s", message.Info.Id))
- }
- portal.markHandled(source, message.Info.Source, eventID, true)
- return true
+
+ return &ConvertedMessage{Intent: intent, Type: event.EventMessage, Content: content}
}
-func (portal *Portal) HandleLocationMessage(source *User, message whatsapp.LocationMessage) bool {
- intent := portal.startHandling(source, message.Info, "location")
- if intent == nil {
- return false
- }
+//func (portal *Portal) HandleStubMessage(source *User, message whatsapp.StubMessage, isBackfill bool) bool {
+// if portal.bridge.Config.Bridge.ChatMetaSync && (!portal.IsBroadcastList() || isBackfill) {
+// // Chat meta sync is enabled, so we use chat update commands and full-syncs instead of message history
+// // However, broadcast lists don't have update commands, so we handle these if it's not a backfill
+// return false
+// }
+// intent := portal.startHandling(source, message.Info, fmt.Sprintf("stub %s", message.Type.String()))
+// if intent == nil {
+// return false
+// }
+// var senderJID string
+// if message.Info.FromMe {
+// senderJID = source.JID
+// } else {
+// senderJID = message.Info.SenderJid
+// }
+// var eventID id.EventID
+// // TODO find more real event IDs
+// // TODO timestamp massaging
+// switch message.Type {
+// case waProto.WebMessageInfo_GROUP_CHANGE_SUBJECT:
+// portal.UpdateName(message.FirstParam, "", intent, true)
+// case waProto.WebMessageInfo_GROUP_CHANGE_ICON:
+// portal.UpdateAvatar(source, nil, true)
+// case waProto.WebMessageInfo_GROUP_CHANGE_DESCRIPTION:
+// if isBackfill {
+// // TODO fetch topic from server
+// }
+// //portal.UpdateTopic(message.FirstParam, "", intent, true)
+// case waProto.WebMessageInfo_GROUP_CHANGE_ANNOUNCE:
+// eventID = portal.RestrictMessageSending(message.FirstParam == "on")
+// case waProto.WebMessageInfo_GROUP_CHANGE_RESTRICT:
+// eventID = portal.RestrictMetadataChanges(message.FirstParam == "on")
+// case waProto.WebMessageInfo_GROUP_PARTICIPANT_ADD, waProto.WebMessageInfo_GROUP_PARTICIPANT_INVITE, waProto.WebMessageInfo_BROADCAST_ADD:
+// eventID = portal.HandleWhatsAppInvite(source, senderJID, intent, message.Params)
+// case waProto.WebMessageInfo_GROUP_PARTICIPANT_REMOVE, waProto.WebMessageInfo_GROUP_PARTICIPANT_LEAVE, waProto.WebMessageInfo_BROADCAST_REMOVE:
+// portal.HandleWhatsAppKick(source, senderJID, message.Params)
+// case waProto.WebMessageInfo_GROUP_PARTICIPANT_PROMOTE:
+// eventID = portal.ChangeAdminStatus(message.Params, true)
+// case waProto.WebMessageInfo_GROUP_PARTICIPANT_DEMOTE:
+// eventID = portal.ChangeAdminStatus(message.Params, false)
+// default:
+// return false
+// }
+// if len(eventID) == 0 {
+// eventID = id.EventID(fmt.Sprintf("net.maunium.whatsapp.fake::%s", message.Info.Id))
+// }
+// portal.markHandled(source, message.Info.Source, eventID, true)
+// return true
+//}
- url := message.Url
+func (portal *Portal) convertLocationMessage(intent *appservice.IntentAPI, msg *waProto.LocationMessage) *ConvertedMessage {
+ url := msg.GetUrl()
if len(url) == 0 {
- url = fmt.Sprintf("https://maps.google.com/?q=%.5f,%.5f", message.DegreesLatitude, message.DegreesLongitude)
+ url = fmt.Sprintf("https://maps.google.com/?q=%.5f,%.5f", msg.GetDegreesLatitude(), msg.GetDegreesLongitude())
}
- name := message.Name
+ name := msg.GetName()
if len(name) == 0 {
latChar := 'N'
- if message.DegreesLatitude < 0 {
+ if msg.GetDegreesLatitude() < 0 {
latChar = 'S'
}
longChar := 'E'
- if message.DegreesLongitude < 0 {
+ if msg.GetDegreesLongitude() < 0 {
longChar = 'W'
}
- name = fmt.Sprintf("%.4f° %c %.4f° %c", math.Abs(message.DegreesLatitude), latChar, math.Abs(message.DegreesLongitude), longChar)
+ name = fmt.Sprintf("%.4f° %c %.4f° %c", math.Abs(msg.GetDegreesLatitude()), latChar, math.Abs(msg.GetDegreesLongitude()), longChar)
}
content := &event.MessageEventContent{
MsgType: event.MsgLocation,
- Body: fmt.Sprintf("Location: %s\n%s\n%s", name, message.Address, url),
+ Body: fmt.Sprintf("Location: %s\n%s\n%s", name, msg.GetAddress(), url),
Format: event.FormatHTML,
- FormattedBody: fmt.Sprintf("Location: %s
%s", url, name, message.Address),
- GeoURI: fmt.Sprintf("geo:%.5f,%.5f", message.DegreesLatitude, message.DegreesLongitude),
+ FormattedBody: fmt.Sprintf("Location: %s
%s", url, name, msg.GetAddress()),
+ GeoURI: fmt.Sprintf("geo:%.5f,%.5f", msg.GetDegreesLatitude(), msg.GetDegreesLongitude()),
}
- if len(message.JpegThumbnail) > 0 {
- thumbnailMime := http.DetectContentType(message.JpegThumbnail)
- uploadedThumbnail, _ := intent.UploadBytes(message.JpegThumbnail, thumbnailMime)
+ if len(msg.GetJpegThumbnail()) > 0 {
+ thumbnailMime := http.DetectContentType(msg.GetJpegThumbnail())
+ uploadedThumbnail, _ := intent.UploadBytes(msg.GetJpegThumbnail(), thumbnailMime)
if uploadedThumbnail != nil {
- cfg, _, _ := image.DecodeConfig(bytes.NewReader(message.JpegThumbnail))
+ cfg, _, _ := image.DecodeConfig(bytes.NewReader(msg.GetJpegThumbnail()))
content.Info = &event.FileInfo{
ThumbnailInfo: &event.FileInfo{
- Size: len(message.JpegThumbnail),
+ Size: len(msg.GetJpegThumbnail()),
Width: cfg.Width,
Height: cfg.Height,
MimeType: thumbnailMime,
@@ -1517,32 +1621,21 @@ func (portal *Portal) HandleLocationMessage(source *User, message whatsapp.Locat
}
}
- portal.SetReply(content, message.ContextInfo)
+ portal.SetReply(content, msg.GetContextInfo().GetStanzaId())
- resp, err := portal.sendMessage(intent, event.EventMessage, content, int64(message.Info.Timestamp*1000))
- if err != nil {
- portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
- } else {
- portal.finishHandling(source, message.Info.Source, resp.EventID)
- }
- return true
+ return &ConvertedMessage{Intent: intent, Type: event.EventMessage, Content: content}
}
-func (portal *Portal) HandleContactMessage(source *User, message whatsapp.ContactMessage) bool {
- intent := portal.startHandling(source, message.Info, "contact")
- if intent == nil {
- return false
- }
-
- fileName := fmt.Sprintf("%s.vcf", message.DisplayName)
- data := []byte(message.Vcard)
+func (portal *Portal) convertContactMessage(intent *appservice.IntentAPI, msg *waProto.ContactMessage) *ConvertedMessage {
+ fileName := fmt.Sprintf("%s.vcf", msg.GetDisplayName())
+ data := []byte(msg.GetVcard())
mimeType := "text/vcard"
data, uploadMimeType, file := portal.encryptFile(data, mimeType)
uploadResp, err := intent.UploadBytesWithName(data, uploadMimeType, fileName)
if err != nil {
- portal.log.Errorfln("Failed to upload vcard of %s: %v", message.DisplayName, err)
- return true
+ portal.log.Errorfln("Failed to upload vcard of %s: %v", msg.GetDisplayName(), err)
+ return nil
}
content := &event.MessageEventContent{
@@ -1551,7 +1644,7 @@ func (portal *Portal) HandleContactMessage(source *User, message whatsapp.Contac
File: file,
Info: &event.FileInfo{
MimeType: mimeType,
- Size: len(message.Vcard),
+ Size: len(msg.GetVcard()),
},
}
if content.File != nil {
@@ -1560,40 +1653,9 @@ func (portal *Portal) HandleContactMessage(source *User, message whatsapp.Contac
content.URL = uploadResp.ContentURI.CUString()
}
- portal.SetReply(content, message.ContextInfo)
+ portal.SetReply(content, msg.GetContextInfo().GetStanzaId())
- resp, err := portal.sendMessage(intent, event.EventMessage, content, int64(message.Info.Timestamp*1000))
- if err != nil {
- portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
- } else {
- portal.finishHandling(source, message.Info.Source, resp.EventID)
- }
- return true
-}
-
-func (portal *Portal) sendMediaBridgeFailure(source *User, intent *appservice.IntentAPI, info whatsapp.MessageInfo, bridgeErr error) {
- portal.log.Errorfln("Failed to bridge media for %s: %v", info.Id, bridgeErr)
- resp, err := portal.sendMessage(intent, event.EventMessage, &event.MessageEventContent{
- MsgType: event.MsgNotice,
- Body: "Failed to bridge media",
- }, int64(info.Timestamp*1000))
- if err != nil {
- portal.log.Errorfln("Failed to send media download error message for %s: %v", info.Id, err)
- } else {
- portal.finishHandling(source, info.Source, resp.EventID)
- }
-}
-
-func (portal *Portal) encryptFile(data []byte, mimeType string) ([]byte, string, *event.EncryptedFileInfo) {
- if !portal.Encrypted {
- return data, mimeType, nil
- }
-
- file := &event.EncryptedFileInfo{
- EncryptedFile: *attachment.NewEncryptedFile(),
- URL: "",
- }
- return file.Encrypt(data), "application/octet-stream", file
+ return &ConvertedMessage{Intent: intent, Type: event.EventMessage, Content: content}
}
func (portal *Portal) tryKickUser(userID id.UserID, intent *appservice.IntentAPI) error {
@@ -1613,11 +1675,11 @@ func (portal *Portal) removeUser(isSameUser bool, kicker *appservice.IntentAPI,
if err != nil {
portal.log.Warnfln("Failed to kick %s from %s: %v", target, portal.MXID, err)
if targetIntent != nil {
- _, _ = targetIntent.LeaveRoom(portal.MXID)
+ _, _ = portal.leaveWithPuppetMeta(targetIntent)
}
}
} else {
- _, err := targetIntent.LeaveRoom(portal.MXID)
+ _, err := portal.leaveWithPuppetMeta(targetIntent)
if err != nil {
portal.log.Warnfln("Failed to leave portal as %s: %v", target, err)
_, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: target})
@@ -1625,11 +1687,11 @@ func (portal *Portal) removeUser(isSameUser bool, kicker *appservice.IntentAPI,
}
}
-func (portal *Portal) HandleWhatsAppKick(source *User, senderJID string, jids []string) {
+func (portal *Portal) HandleWhatsAppKick(source *User, senderJID types.JID, jids []types.JID) {
sender := portal.bridge.GetPuppetByJID(senderJID)
senderIntent := sender.IntentFor(portal)
for _, jid := range jids {
- if source != nil && source.JID == jid {
+ if source != nil && source.JID.User == jid.User {
portal.log.Debugln("Ignoring self-kick by", source.MXID)
continue
}
@@ -1649,17 +1711,27 @@ func (portal *Portal) HandleWhatsAppKick(source *User, senderJID string, jids []
}
}
-func (portal *Portal) HandleWhatsAppInvite(source *User, senderJID string, intent *appservice.IntentAPI, jids []string) (evtID id.EventID) {
- if intent == nil {
- intent = portal.MainIntent()
- if senderJID != "unknown" {
- sender := portal.bridge.GetPuppetByJID(senderJID)
- intent = sender.IntentFor(portal)
- }
+func (portal *Portal) leaveWithPuppetMeta(intent *appservice.IntentAPI) (*mautrix.RespSendEvent, error) {
+ content := event.Content{
+ Parsed: event.MemberEventContent{
+ Membership: event.MembershipLeave,
+ },
+ Raw: map[string]interface{}{
+ "net.maunium.whatsapp.puppet": true,
+ },
+ }
+ return intent.SendStateEvent(portal.MXID, event.StateMember, intent.UserID.String(), &content)
+}
+
+func (portal *Portal) HandleWhatsAppInvite(source *User, senderJID *types.JID, jids []types.JID) (evtID id.EventID) {
+ intent := portal.MainIntent()
+ if senderJID != nil && !senderJID.IsEmpty() {
+ sender := portal.bridge.GetPuppetByJID(*senderJID)
+ intent = sender.IntentFor(portal)
}
for _, jid := range jids {
puppet := portal.bridge.GetPuppetByJID(jid)
- puppet.SyncContactIfNecessary(source)
+ puppet.SyncContact(source, true)
content := event.Content{
Parsed: event.MemberEventContent{
Membership: "invite",
@@ -1685,110 +1757,152 @@ func (portal *Portal) HandleWhatsAppInvite(source *User, senderJID string, inten
return
}
-type base struct {
- download func() ([]byte, error)
- info whatsapp.MessageInfo
- context whatsapp.ContextInfo
- mimeType string
+func (portal *Portal) makeMediaBridgeFailureMessage(intent *appservice.IntentAPI, info *types.MessageInfo, bridgeErr error, captionContent *event.MessageEventContent) *ConvertedMessage {
+ portal.log.Errorfln("Failed to bridge media for %s: %v", info.ID, bridgeErr)
+ return &ConvertedMessage{Intent: intent, Type: event.EventMessage, Content: &event.MessageEventContent{
+ MsgType: event.MsgNotice,
+ Body: "Failed to bridge media",
+ }, Caption: captionContent}
}
-type mediaMessage struct {
- base
-
- thumbnail []byte
- caption string
- fileName string
- length uint32
- sendAsSticker bool
-}
-
-func (portal *Portal) HandleMediaMessage(source *User, msg mediaMessage) bool {
- intent := portal.startHandling(source, msg.info, fmt.Sprintf("media %s", msg.mimeType))
- if intent == nil {
- return false
+func (portal *Portal) encryptFile(data []byte, mimeType string) ([]byte, string, *event.EncryptedFileInfo) {
+ if !portal.Encrypted {
+ return data, mimeType, nil
}
- data, err := msg.download()
- if errors.Is(err, whatsapp.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsapp.ErrMediaDownloadFailedWith410) {
- portal.log.Warnfln("Failed to download media for %s: %v. Calling LoadMediaInfo and retrying download...", msg.info.Id, err)
- _, err = source.Conn.LoadMediaInfo(msg.info.RemoteJid, msg.info.Id, msg.info.FromMe)
- if err != nil {
- portal.sendMediaBridgeFailure(source, intent, msg.info, fmt.Errorf("failed to load media info: %w", err))
- return true
+ file := &event.EncryptedFileInfo{
+ EncryptedFile: *attachment.NewEncryptedFile(),
+ URL: "",
+ }
+ return file.Encrypt(data), "application/octet-stream", file
+}
+
+type MediaMessage interface {
+ whatsmeow.DownloadableMessage
+ GetContextInfo() *waProto.ContextInfo
+ GetMimetype() string
+}
+
+type MediaMessageWithThumbnail interface {
+ MediaMessage
+ GetJpegThumbnail() []byte
+ GetCaption() string
+}
+
+type MediaMessageWithCaption interface {
+ MediaMessage
+ GetCaption() string
+}
+
+type MediaMessageWithFileName interface {
+ MediaMessage
+ GetFileName() string
+}
+
+type MediaMessageWithDuration interface {
+ MediaMessage
+ GetSeconds() uint32
+}
+
+func (portal *Portal) convertMediaMessage(intent *appservice.IntentAPI, source *User, info *types.MessageInfo, msg MediaMessage) *ConvertedMessage {
+ messageWithCaption, ok := msg.(MediaMessageWithCaption)
+ var captionContent *event.MessageEventContent
+ if ok && len(messageWithCaption.GetCaption()) > 0 {
+ captionContent = &event.MessageEventContent{
+ Body: messageWithCaption.GetCaption(),
+ MsgType: event.MsgNotice,
}
- data, err = msg.download()
+
+ portal.bridge.Formatter.ParseWhatsApp(captionContent, msg.GetContextInfo().GetMentionedJid())
}
- if errors.Is(err, whatsapp.ErrNoURLPresent) {
- portal.log.Debugfln("No URL present error for media message %s, ignoring...", msg.info.Id)
- return true
- } else if errors.Is(err, whatsapp.ErrInvalidMediaHMAC) || errors.Is(err, whatsapp.ErrFileLengthMismatch) {
- portal.log.Warnfln("Got error '%v' while downloading media in %s, but official WhatsApp clients don't seem to care, so ignoring that error and bridging file anyway", err, msg.info.Id)
+
+ data, err := source.Client.Download(msg)
+ // TODO can these errors still be handled?
+ //if errors.Is(err, whatsapp.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsapp.ErrMediaDownloadFailedWith410) {
+ // portal.log.Warnfln("Failed to download media for %s: %v. Calling LoadMediaInfo and retrying download...", msg.info.Id, err)
+ // _, err = source.Conn.LoadMediaInfo(msg.info.RemoteJid, msg.info.Id, msg.info.FromMe)
+ // if err != nil {
+ // portal.sendMediaBridgeFailure(source, intent, msg.info, fmt.Errorf("failed to load media info: %w", err))
+ // return true
+ // }
+ // data, err = msg.download()
+ //}
+ if errors.Is(err, whatsmeow.ErrNoURLPresent) {
+ portal.log.Debugfln("No URL present error for media message %s, ignoring...", info.ID)
+ return nil
} else if err != nil {
- portal.sendMediaBridgeFailure(source, intent, msg.info, err)
- return true
+ return portal.makeMediaBridgeFailureMessage(intent, info, err, captionContent)
}
var width, height int
- if strings.HasPrefix(msg.mimeType, "image/") {
+ if strings.HasPrefix(msg.GetMimetype(), "image/") {
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
width, height = cfg.Width, cfg.Height
}
- data, uploadMimeType, file := portal.encryptFile(data, msg.mimeType)
+ data, uploadMimeType, file := portal.encryptFile(data, msg.GetMimetype())
uploaded, err := intent.UploadBytes(data, uploadMimeType)
if err != nil {
if errors.Is(err, mautrix.MTooLarge) {
- portal.sendMediaBridgeFailure(source, intent, msg.info, errors.New("homeserver rejected too large file"))
+ return portal.makeMediaBridgeFailureMessage(intent, info, errors.New("homeserver rejected too large file"), captionContent)
} else if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.IsStatus(413) {
- portal.sendMediaBridgeFailure(source, intent, msg.info, errors.New("proxy rejected too large file"))
+ return portal.makeMediaBridgeFailureMessage(intent, info, errors.New("proxy rejected too large file"), captionContent)
} else {
- portal.sendMediaBridgeFailure(source, intent, msg.info, fmt.Errorf("failed to upload media: %w", err))
- }
- return true
- }
-
- if msg.fileName == "" {
- mimeClass := strings.Split(msg.mimeType, "/")[0]
- switch mimeClass {
- case "application":
- msg.fileName = "file"
- default:
- msg.fileName = mimeClass
- }
-
- exts, _ := mime.ExtensionsByType(msg.mimeType)
- if exts != nil && len(exts) > 0 {
- msg.fileName += exts[0]
+ return portal.makeMediaBridgeFailureMessage(intent, info, fmt.Errorf("failed to upload media: %w", err), captionContent)
}
}
content := &event.MessageEventContent{
- Body: msg.fileName,
File: file,
Info: &event.FileInfo{
Size: len(data),
- MimeType: msg.mimeType,
+ MimeType: msg.GetMimetype(),
Width: width,
Height: height,
- Duration: int(msg.length),
},
}
+
+ msgWithName, ok := msg.(MediaMessageWithFileName)
+ if ok && len(msgWithName.GetFileName()) > 0 {
+ content.Body = msgWithName.GetFileName()
+ } else {
+ mimeClass := strings.Split(msg.GetMimetype(), "/")[0]
+ switch mimeClass {
+ case "application":
+ content.Body = "file"
+ default:
+ content.Body = mimeClass
+ }
+
+ exts, _ := mime.ExtensionsByType(msg.GetMimetype())
+ if exts != nil && len(exts) > 0 {
+ content.Body += exts[0]
+ }
+ }
+
+ msgWithDuration, ok := msg.(MediaMessageWithDuration)
+ if ok {
+ content.Info.Duration = int(msgWithDuration.GetSeconds()) * 1000
+ }
+
if content.File != nil {
content.File.URL = uploaded.ContentURI.CUString()
} else {
content.URL = uploaded.ContentURI.CUString()
}
- portal.SetReply(content, msg.context)
+ portal.SetReply(content, msg.GetContextInfo().GetStanzaId())
- if msg.thumbnail != nil && portal.bridge.Config.Bridge.WhatsappThumbnail {
- thumbnailMime := http.DetectContentType(msg.thumbnail)
- thumbnailCfg, _, _ := image.DecodeConfig(bytes.NewReader(msg.thumbnail))
- thumbnailSize := len(msg.thumbnail)
- thumbnail, thumbnailUploadMime, thumbnailFile := portal.encryptFile(msg.thumbnail, thumbnailMime)
+ messageWithThumbnail, ok := msg.(MediaMessageWithThumbnail)
+ if ok && messageWithThumbnail.GetJpegThumbnail() != nil && portal.bridge.Config.Bridge.WhatsappThumbnail {
+ thumbnailData := messageWithThumbnail.GetJpegThumbnail()
+ thumbnailMime := http.DetectContentType(thumbnailData)
+ thumbnailCfg, _, _ := image.DecodeConfig(bytes.NewReader(thumbnailData))
+ thumbnailSize := len(thumbnailData)
+ thumbnail, thumbnailUploadMime, thumbnailFile := portal.encryptFile(thumbnailData, thumbnailMime)
uploadedThumbnail, err := intent.UploadBytes(thumbnail, thumbnailUploadMime)
if err != nil {
- portal.log.Warnfln("Failed to upload thumbnail for %s: %v", msg.info.Id, err)
+ portal.log.Warnfln("Failed to upload thumbnail for %s: %v", info.ID, err)
} else if uploadedThumbnail != nil {
if thumbnailFile != nil {
thumbnailFile.URL = uploadedThumbnail.ContentURI.CUString()
@@ -1805,9 +1919,10 @@ func (portal *Portal) HandleMediaMessage(source *User, msg mediaMessage) bool {
}
}
- switch strings.ToLower(strings.Split(msg.mimeType, "/")[0]) {
+ _, isSticker := msg.(*waProto.StickerMessage)
+ switch strings.ToLower(strings.Split(msg.GetMimetype(), "/")[0]) {
case "image":
- if !msg.sendAsSticker {
+ if !isSticker {
content.MsgType = event.MsgImage
}
case "video":
@@ -1818,40 +1933,17 @@ func (portal *Portal) HandleMediaMessage(source *User, msg mediaMessage) bool {
content.MsgType = event.MsgFile
}
- ts := int64(msg.info.Timestamp * 1000)
eventType := event.EventMessage
- if msg.sendAsSticker {
+ if isSticker {
eventType = event.EventSticker
}
- resp, err := portal.sendMessage(intent, eventType, content, ts)
- if err != nil {
- portal.log.Errorfln("Failed to handle message %s: %v", msg.info.Id, err)
- return true
+
+ return &ConvertedMessage{
+ Intent: intent,
+ Type: eventType,
+ Content: content,
+ Caption: captionContent,
}
-
- if len(msg.caption) > 0 {
- captionContent := &event.MessageEventContent{
- Body: msg.caption,
- MsgType: event.MsgNotice,
- }
-
- portal.bridge.Formatter.ParseWhatsApp(captionContent, msg.context.MentionedJID)
-
- resp, err = portal.sendMessage(intent, event.EventMessage, captionContent, ts)
- if err != nil {
- portal.log.Warnfln("Failed to handle caption of message %s: %v", msg.info.Id, err)
- }
- }
-
- portal.finishHandling(source, msg.info.Source, resp.EventID)
- return true
-}
-
-func makeMessageID() *string {
- b := make([]byte, 10)
- rand.Read(b)
- str := strings.ToUpper(hex.EncodeToString(b))
- return &str
}
func (portal *Portal) downloadThumbnail(content *event.MessageEventContent, id id.EventID) []byte {
@@ -1951,9 +2043,9 @@ func (portal *Portal) convertGifToVideo(gif []byte) ([]byte, error) {
return mp4, nil
}
-func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool, content *event.MessageEventContent, eventID id.EventID, mediaType whatsapp.MediaType) *MediaUpload {
+func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool, content *event.MessageEventContent, eventID id.EventID, mediaType whatsmeow.MediaType) *MediaUpload {
var caption string
- var mentionedJIDs []whatsapp.JID
+ var mentionedJIDs []string
if relaybotFormatted {
caption, mentionedJIDs = portal.bridge.Formatter.ParseMatrix(content.FormattedBody)
}
@@ -1981,7 +2073,7 @@ func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool
return nil
}
}
- if mediaType == whatsapp.MediaVideo && content.GetInfo().MimeType == "image/gif" {
+ if mediaType == whatsmeow.MediaVideo && content.GetInfo().MimeType == "image/gif" {
data, err = portal.convertGifToVideo(data)
if err != nil {
portal.log.Errorfln("Failed to convert gif to mp4 in %s: %v", eventID, err)
@@ -1989,7 +2081,7 @@ func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool
}
content.Info.MimeType = "video/mp4"
}
- if mediaType == whatsapp.MediaImage && content.GetInfo().MimeType == "image/webp" {
+ if mediaType == whatsmeow.MediaImage && content.GetInfo().MimeType == "image/webp" {
data, err = portal.convertWebPtoPNG(data)
if err != nil {
portal.log.Errorfln("Failed to convert webp to png in %s: %v", eventID, err)
@@ -1997,40 +2089,34 @@ func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool
}
content.Info.MimeType = "image/png"
}
- url, mediaKey, fileEncSHA256, fileSHA256, fileLength, err := sender.Conn.Upload(bytes.NewReader(data), mediaType)
+ uploadResp, err := sender.Client.Upload(context.Background(), data, mediaType)
if err != nil {
portal.log.Errorfln("Failed to upload media in %s: %v", eventID, err)
return nil
}
return &MediaUpload{
- Caption: caption,
- MentionedJIDs: mentionedJIDs,
- URL: url,
- MediaKey: mediaKey,
- FileEncSHA256: fileEncSHA256,
- FileSHA256: fileSHA256,
- FileLength: fileLength,
- Thumbnail: portal.downloadThumbnail(content, eventID),
+ UploadResponse: uploadResp,
+ Caption: caption,
+ MentionedJIDs: mentionedJIDs,
+ Thumbnail: portal.downloadThumbnail(content, eventID),
+ FileLength: len(data),
}
}
type MediaUpload struct {
+ whatsmeow.UploadResponse
Caption string
- MentionedJIDs []whatsapp.JID
- URL string
- MediaKey []byte
- FileEncSHA256 []byte
- FileSHA256 []byte
- FileLength uint64
+ MentionedJIDs []string
Thumbnail []byte
+ FileLength int
}
func (portal *Portal) sendMatrixConnectionError(sender *User, eventID id.EventID) bool {
if !sender.HasSession() {
portal.log.Debugln("Ignoring event", eventID, "from", sender.MXID, "as user has no session")
return true
- } else if !sender.IsConnected() {
+ } /*else if !sender.IsConnected() {
inRoom := ""
if portal.IsPrivateChat() {
inRoom = " in your management room"
@@ -2051,7 +2137,8 @@ func (portal *Portal) sendMatrixConnectionError(sender *User, eventID id.EventID
portal.log.Errorln("Failed to send bridging failure message:", err)
}
return true
- }
+ }*/
+ // FIXME implement
return false
}
@@ -2065,7 +2152,7 @@ func (portal *Portal) addRelaybotFormat(sender *User, content *event.MessageEven
content.FormattedBody = strings.Replace(html.EscapeString(content.Body), "\n", "
", -1)
content.Format = event.FormatHTML
}
- data, err := portal.bridge.Config.Bridge.Relaybot.FormatMessage(content, sender.MXID, member)
+ data, err := portal.bridge.Config.Bridge.Relay.FormatMessage(content, sender.MXID, member)
if err != nil {
portal.log.Errorln("Failed to apply relaybot format:", err)
}
@@ -2109,35 +2196,22 @@ func fallbackQuoteContent() *waProto.Message {
}
}
-func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waProto.WebMessageInfo, *User) {
+func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waProto.Message, *User) {
content, ok := evt.Content.Parsed.(*event.MessageEventContent)
if !ok {
portal.log.Debugfln("Failed to handle event %s: unexpected parsed content type %T", evt.ID, evt.Content.Parsed)
return nil, sender
}
- ts := uint64(evt.Timestamp / 1000)
- status := waProto.WebMessageInfo_PENDING
- trueVal := true
- info := &waProto.WebMessageInfo{
- Key: &waProto.MessageKey{
- FromMe: &trueVal,
- Id: makeMessageID(),
- RemoteJid: &portal.Key.JID,
- },
- MessageTimestamp: &ts,
- MessageC2STimestamp: &ts,
- Message: &waProto.Message{},
- Status: &status,
- }
- ctxInfo := &waProto.ContextInfo{}
+ var msg waProto.Message
+ var ctxInfo waProto.ContextInfo
replyToID := content.GetReplyTo()
if len(replyToID) > 0 {
content.RemoveReplyFallback()
- msg := portal.bridge.DB.Message.GetByMXID(replyToID)
- if msg != nil {
- ctxInfo.StanzaId = &msg.JID
- ctxInfo.Participant = &msg.Sender
+ replyToMsg := portal.bridge.DB.Message.GetByMXID(replyToID)
+ if replyToMsg != nil {
+ ctxInfo.StanzaId = &replyToMsg.JID
+ ctxInfo.Participant = proto.String(replyToMsg.Sender.String())
// Using blank content here seems to work fine on all official WhatsApp apps.
// Getting the content from the phone would be possible, but it's complicated.
// https://github.com/mautrix/whatsapp/commit/b3312bc663772aa274cea90ffa773da2217bb5e0
@@ -2145,18 +2219,13 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
}
}
relaybotFormatted := false
- if sender.NeedsRelaybot(portal) {
+ if !sender.HasSession() {
if !portal.HasRelaybot() {
- if sender.HasSession() {
- portal.log.Debugln("Database says", sender.MXID, "not in chat and no relaybot, but trying to send anyway")
- } else {
- portal.log.Debugln("Ignoring message from", sender.MXID, "in chat with no relaybot")
- return nil, sender
- }
- } else {
- relaybotFormatted = portal.addRelaybotFormat(sender, content)
- sender = portal.bridge.Relaybot
+ portal.log.Debugln("Ignoring message from", sender.MXID, "in chat with no relaybot")
+ return nil, sender
}
+ relaybotFormatted = portal.addRelaybotFormat(sender, content)
+ sender = portal.GetRelayUser()
}
if evt.Type == event.EventSticker {
content.MsgType = event.MsgImage
@@ -2178,21 +2247,21 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
text = "/me " + text
}
if ctxInfo.StanzaId != nil || ctxInfo.MentionedJid != nil {
- info.Message.ExtendedTextMessage = &waProto.ExtendedTextMessage{
+ msg.ExtendedTextMessage = &waProto.ExtendedTextMessage{
Text: &text,
- ContextInfo: ctxInfo,
+ ContextInfo: &ctxInfo,
}
} else {
- info.Message.Conversation = &text
+ msg.Conversation = &text
}
case event.MsgImage:
- media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaImage)
+ media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsmeow.MediaImage)
if media == nil {
return nil, sender
}
ctxInfo.MentionedJid = media.MentionedJIDs
- info.Message.ImageMessage = &waProto.ImageMessage{
- ContextInfo: ctxInfo,
+ msg.ImageMessage = &waProto.ImageMessage{
+ ContextInfo: &ctxInfo,
Caption: &media.Caption,
JpegThumbnail: media.Thumbnail,
Url: &media.URL,
@@ -2200,18 +2269,18 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
Mimetype: &content.GetInfo().MimeType,
FileEncSha256: media.FileEncSHA256,
FileSha256: media.FileSHA256,
- FileLength: &media.FileLength,
+ FileLength: proto.Uint64(uint64(media.FileLength)),
}
case event.MsgVideo:
gifPlayback := content.GetInfo().MimeType == "image/gif"
- media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaVideo)
+ media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsmeow.MediaVideo)
if media == nil {
return nil, sender
}
duration := uint32(content.GetInfo().Duration / 1000)
ctxInfo.MentionedJid = media.MentionedJIDs
- info.Message.VideoMessage = &waProto.VideoMessage{
- ContextInfo: ctxInfo,
+ msg.VideoMessage = &waProto.VideoMessage{
+ ContextInfo: &ctxInfo,
Caption: &media.Caption,
JpegThumbnail: media.Thumbnail,
Url: &media.URL,
@@ -2221,39 +2290,38 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
Seconds: &duration,
FileEncSha256: media.FileEncSHA256,
FileSha256: media.FileSHA256,
- FileLength: &media.FileLength,
+ FileLength: proto.Uint64(uint64(media.FileLength)),
}
case event.MsgAudio:
- media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaAudio)
+ media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsmeow.MediaAudio)
if media == nil {
return nil, sender
}
duration := uint32(content.GetInfo().Duration / 1000)
- info.Message.AudioMessage = &waProto.AudioMessage{
- ContextInfo: ctxInfo,
+ msg.AudioMessage = &waProto.AudioMessage{
+ ContextInfo: &ctxInfo,
Url: &media.URL,
MediaKey: media.MediaKey,
Mimetype: &content.GetInfo().MimeType,
Seconds: &duration,
FileEncSha256: media.FileEncSHA256,
FileSha256: media.FileSHA256,
- FileLength: &media.FileLength,
+ FileLength: proto.Uint64(uint64(media.FileLength)),
}
_, isMSC3245Voice := evt.Content.Raw["org.matrix.msc3245.voice"]
_, isMSC2516Voice := evt.Content.Raw["org.matrix.msc2516.voice"]
if isMSC3245Voice || isMSC2516Voice {
- info.Message.AudioMessage.Ptt = &trueVal
+ msg.AudioMessage.Ptt = proto.Bool(true)
// hacky hack to add the codecs param that whatsapp seems to require
- mimeWithCodec := addCodecToMime(content.GetInfo().MimeType, "opus")
- info.Message.AudioMessage.Mimetype = &mimeWithCodec
+ msg.AudioMessage.Mimetype = proto.String(addCodecToMime(content.GetInfo().MimeType, "opus"))
}
case event.MsgFile:
- media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaDocument)
+ media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsmeow.MediaDocument)
if media == nil {
return nil, sender
}
- info.Message.DocumentMessage = &waProto.DocumentMessage{
- ContextInfo: ctxInfo,
+ msg.DocumentMessage = &waProto.DocumentMessage{
+ ContextInfo: &ctxInfo,
Url: &media.URL,
Title: &content.Body,
FileName: &content.Body,
@@ -2261,7 +2329,7 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
Mimetype: &content.GetInfo().MimeType,
FileEncSha256: media.FileEncSHA256,
FileSha256: media.FileSHA256,
- FileLength: &media.FileLength,
+ FileLength: proto.Uint64(uint64(media.FileLength)),
}
case event.MsgLocation:
lat, long, err := parseGeoURI(content.GeoURI)
@@ -2269,28 +2337,17 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
portal.log.Debugfln("Invalid geo URI on Matrix event %s: %v", evt.ID, err)
return nil, sender
}
- info.Message.LocationMessage = &waProto.LocationMessage{
+ msg.LocationMessage = &waProto.LocationMessage{
DegreesLatitude: &lat,
DegreesLongitude: &long,
Comment: &content.Body,
- ContextInfo: ctxInfo,
+ ContextInfo: &ctxInfo,
}
default:
portal.log.Debugfln("Unhandled Matrix event %s: unknown msgtype %s", evt.ID, content.MsgType)
return nil, sender
}
- return info, sender
-}
-
-func (portal *Portal) wasMessageSent(sender *User, id string) bool {
- _, err := sender.Conn.LoadMessagesAfter(portal.Key.JID, id, true, 0)
- if err != nil {
- if err != whatsapp.ErrServerRespondedWith404 {
- portal.log.Warnfln("Failed to check if message was bridged without response: %v", err)
- }
- return false
- }
- return true
+ return &msg, sender
}
func (portal *Portal) sendErrorMessage(message string, confirmed bool) id.EventID {
@@ -2318,120 +2375,64 @@ func (portal *Portal) sendDeliveryReceipt(eventID id.EventID) {
}
}
+func (portal *Portal) generateMessageInfo(sender *User) *types.MessageInfo {
+ return &types.MessageInfo{
+ ID: whatsmeow.GenerateMessageID(),
+ Timestamp: time.Now(),
+ MessageSource: types.MessageSource{
+ Sender: sender.JID,
+ Chat: portal.Key.JID,
+ IsFromMe: true,
+ IsGroup: portal.Key.JID.Server == types.GroupServer || portal.Key.JID.Server == types.BroadcastServer,
+ },
+ }
+}
+
func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) {
if !portal.HasRelaybot() &&
- ((portal.IsPrivateChat() && sender.JID != portal.Key.Receiver) ||
+ ((portal.IsPrivateChat() && sender.JID.User != portal.Key.Receiver.User) ||
portal.sendMatrixConnectionError(sender, evt.ID)) {
return
}
portal.log.Debugfln("Received event %s", evt.ID)
- info, sender := portal.convertMatrixMessage(sender, evt)
- if info == nil {
+ msg, sender := portal.convertMatrixMessage(sender, evt)
+ if msg == nil {
return
}
- dbMsg := portal.markHandled(sender, info, evt.ID, false)
- portal.sendRaw(sender, evt, info, dbMsg)
-}
-
-func (portal *Portal) sendRaw(sender *User, evt *event.Event, info *waProto.WebMessageInfo, dbMsg *database.Message) {
- portal.log.Debugln("Sending event", evt.ID, "to WhatsApp", info.Key.GetId())
- errChan := make(chan error, 1)
- go sender.Conn.SendRaw(info, errChan)
-
- var err error
- var errorEventID id.EventID
- select {
- case err = <-errChan:
- case <-time.After(time.Duration(portal.bridge.Config.Bridge.ConnectionTimeout) * time.Second):
- if portal.bridge.Config.Bridge.FetchMessageOnTimeout && portal.wasMessageSent(sender, info.Key.GetId()) {
- portal.log.Debugln("Matrix event %s was bridged, but response didn't arrive within timeout")
- portal.sendDeliveryReceipt(evt.ID)
- } else {
- portal.log.Warnfln("Response when bridging Matrix event %s is taking long to arrive", evt.ID)
- errorEventID = portal.sendErrorMessage("message sending timed out", false)
- }
- err = <-errChan
- }
+ info := portal.generateMessageInfo(sender)
+ dbMsg := portal.markHandled(nil, info, evt.ID, false, true, false)
+ portal.log.Debugln("Sending event", evt.ID, "to WhatsApp", info.ID)
+ err := sender.Client.SendMessage(portal.Key.JID, info.ID, msg)
if err != nil {
- var statusErr whatsapp.StatusResponse
- errors.As(err, &statusErr)
- var confirmed bool
- var errMsg string
- switch statusErr.Status {
- case 400:
- portal.log.Errorfln("400 response handling Matrix event %s: %+v", evt.ID, statusErr.Extra)
- errMsg = "WhatsApp rejected the message (status code 400)."
- if info.Message.ImageMessage != nil || info.Message.VideoMessage != nil || info.Message.AudioMessage != nil || info.Message.DocumentMessage != nil {
- errMsg += " The attachment type you sent may be unsupported."
- }
- confirmed = true
- case 599:
- errMsg = "WhatsApp rate-limited the message (status code 599)."
- default:
- portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err)
- errMsg = err.Error()
- }
- portal.sendErrorMessage(errMsg, confirmed)
+ portal.log.Errorln("Error sending message: %v", err)
+ portal.sendErrorMessage(err.Error(), true)
} else {
portal.log.Debugfln("Handled Matrix event %s", evt.ID)
portal.sendDeliveryReceipt(evt.ID)
dbMsg.MarkSent()
}
- if errorEventID != "" {
- _, err = portal.MainIntent().RedactEvent(portal.MXID, errorEventID)
- if err != nil {
- portal.log.Warnfln("Failed to redact timeout warning message %s: %v", errorEventID, err)
- }
- }
}
func (portal *Portal) HandleMatrixRedaction(sender *User, evt *event.Event) {
- if portal.IsPrivateChat() && sender.JID != portal.Key.Receiver {
+ if portal.IsPrivateChat() && sender.JID.User != portal.Key.Receiver.User {
return
}
msg := portal.bridge.DB.Message.GetByMXID(evt.Redacts)
- if msg == nil || msg.Sender != sender.JID {
+ if msg == nil {
+ portal.log.Debugfln("Ignoring redaction %s of unknown event by %s", msg, sender.MXID)
+ return
+ } else if msg.Sender.User != sender.JID.User {
+ portal.log.Debugfln("Ignoring redaction %s of %s/%s by %s: message was sent by someone else (%s, not %s)", evt.ID, msg.MXID, msg.JID, sender.MXID, msg.Sender, sender.JID)
return
}
- ts := uint64(evt.Timestamp / 1000)
- status := waProto.WebMessageInfo_PENDING
- protoMsgType := waProto.ProtocolMessage_REVOKE
- fromMe := true
- info := &waProto.WebMessageInfo{
- Key: &waProto.MessageKey{
- FromMe: &fromMe,
- Id: makeMessageID(),
- RemoteJid: &portal.Key.JID,
- },
- MessageTimestamp: &ts,
- Message: &waProto.Message{
- ProtocolMessage: &waProto.ProtocolMessage{
- Type: &protoMsgType,
- Key: &waProto.MessageKey{
- FromMe: &fromMe,
- Id: &msg.JID,
- RemoteJid: &portal.Key.JID,
- },
- },
- },
- Status: &status,
- }
- errChan := make(chan error, 1)
- go sender.Conn.SendRaw(info, errChan)
-
- var err error
- select {
- case err = <-errChan:
- case <-time.After(time.Duration(portal.bridge.Config.Bridge.ConnectionTimeout) * time.Second):
- portal.log.Warnfln("Response when bridging Matrix redaction %s is taking long to arrive", evt.ID)
- err = <-errChan
- }
+ portal.log.Debugfln("Sending redaction %s of %s/%s to WhatsApp", evt.ID, msg.MXID, msg.JID)
+ err := sender.Client.RevokeMessage(portal.Key.JID, msg.JID)
if err != nil {
portal.log.Errorfln("Error handling Matrix redaction %s: %v", evt.ID, err)
} else {
- portal.log.Debugln("Handled Matrix redaction %s of %s", evt.ID, evt.Redacts)
+ portal.log.Debugfln("Handled Matrix redaction %s of %s", evt.ID, evt.Redacts)
portal.sendDeliveryReceipt(evt.ID)
}
}
@@ -2523,12 +2524,13 @@ func (portal *Portal) HandleMatrixLeave(sender *User) {
return
} else if portal.bridge.Config.Bridge.BridgeMatrixLeave {
// TODO should we somehow deduplicate this call if this leave was sent by the bridge?
- resp, err := sender.Conn.LeaveGroup(portal.Key.JID)
- if err != nil {
- portal.log.Errorfln("Failed to leave group as %s: %v", sender.MXID, err)
- return
- }
- portal.log.Infoln("Leave response:", <-resp)
+ // FIXME reimplement
+ //resp, err := sender.Client.LeaveGroup(portal.Key.JID)
+ //if err != nil {
+ // portal.log.Errorfln("Failed to leave group as %s: %v", sender.MXID, err)
+ // return
+ //}
+ //portal.log.Infoln("Leave response:", <-resp)
}
portal.CleanupIfEmpty()
}
@@ -2536,50 +2538,53 @@ func (portal *Portal) HandleMatrixLeave(sender *User) {
func (portal *Portal) HandleMatrixKick(sender *User, evt *event.Event) {
puppet := portal.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey()))
if puppet != nil {
- resp, err := sender.Conn.RemoveMember(portal.Key.JID, []string{puppet.JID})
- if err != nil {
- portal.log.Errorfln("Failed to kick %s from group as %s: %v", puppet.JID, sender.MXID, err)
- return
- }
- portal.log.Infoln("Kick %s response: %s", puppet.JID, <-resp)
+ // FIXME reimplement
+ //resp, err := sender.Conn.RemoveMember(portal.Key.JID, []string{puppet.JID})
+ //if err != nil {
+ // portal.log.Errorfln("Failed to kick %s from group as %s: %v", puppet.JID, sender.MXID, err)
+ // return
+ //}
+ //portal.log.Infoln("Kick %s response: %s", puppet.JID, <-resp)
}
}
func (portal *Portal) HandleMatrixInvite(sender *User, evt *event.Event) {
puppet := portal.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey()))
if puppet != nil {
- resp, err := sender.Conn.AddMember(portal.Key.JID, []string{puppet.JID})
- if err != nil {
- portal.log.Errorfln("Failed to add %s to group as %s: %v", puppet.JID, sender.MXID, err)
- return
- }
- portal.log.Infofln("Add %s response: %s", puppet.JID, <-resp)
+ // FIXME reimplement
+ //resp, err := sender.Conn.AddMember(portal.Key.JID, []string{puppet.JID})
+ //if err != nil {
+ // portal.log.Errorfln("Failed to add %s to group as %s: %v", puppet.JID, sender.MXID, err)
+ // return
+ //}
+ //portal.log.Infofln("Add %s response: %s", puppet.JID, <-resp)
}
}
func (portal *Portal) HandleMatrixMeta(sender *User, evt *event.Event) {
- var resp <-chan string
var err error
switch content := evt.Content.Parsed.(type) {
case *event.RoomNameEventContent:
if content.Name == portal.Name {
return
}
- portal.Name = content.Name
- resp, err = sender.Conn.UpdateGroupSubject(content.Name, portal.Key.JID)
+ // FIXME reimplement
+ //portal.Name = content.Name
+ //resp, err = sender.Conn.UpdateGroupSubject(content.Name, portal.Key.JID)
case *event.TopicEventContent:
if content.Topic == portal.Topic {
return
}
- portal.Topic = content.Topic
- resp, err = sender.Conn.UpdateGroupDescription(sender.JID, portal.Key.JID, content.Topic)
+ // FIXME reimplement
+ //portal.Topic = content.Topic
+ //resp, err = sender.Conn.UpdateGroupDescription(sender.JID, portal.Key.JID, content.Topic)
case *event.RoomAvatarEventContent:
return
}
if err != nil {
portal.log.Errorln("Failed to update metadata:", err)
} else {
- out := <-resp
- portal.log.Debugln("Successfully updated metadata:", out)
+ //out := <-resp
+ //portal.log.Debugln("Successfully updated metadata:", out)
}
}
diff --git a/provisioning.go b/provisioning.go
index deb1629..7e1e996 100644
--- a/provisioning.go
+++ b/provisioning.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -29,7 +29,7 @@ import (
"github.com/gorilla/websocket"
- "github.com/Rhymen/go-whatsapp"
+ "go.mau.fi/whatsmeow"
log "maunium.net/go/maulogger/v2"
@@ -50,11 +50,13 @@ func (prov *ProvisioningAPI) Init() {
r.HandleFunc("/login", prov.Login).Methods(http.MethodGet)
r.HandleFunc("/logout", prov.Logout).Methods(http.MethodPost)
r.HandleFunc("/delete_session", prov.DeleteSession).Methods(http.MethodPost)
- r.HandleFunc("/delete_connection", prov.DeleteConnection).Methods(http.MethodPost)
r.HandleFunc("/disconnect", prov.Disconnect).Methods(http.MethodPost)
r.HandleFunc("/reconnect", prov.Reconnect).Methods(http.MethodPost)
prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.asmux/ping", prov.BridgeStatePing).Methods(http.MethodPost)
prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.bridge_state", prov.BridgeStatePing).Methods(http.MethodPost)
+
+ // Deprecated, just use /disconnect
+ r.HandleFunc("/delete_connection", prov.Disconnect).Methods(http.MethodPost)
}
type responseWrap struct {
@@ -122,7 +124,7 @@ type Response struct {
func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
- if user.Session == nil && user.Conn == nil {
+ if user.Session == nil && user.Client == nil {
jsonResponse(w, http.StatusNotFound, Error{
Error: "Nothing to purge: no session information stored and no active connection.",
ErrCode: "no session",
@@ -130,128 +132,43 @@ func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Reques
return
}
user.DeleteConnection()
- user.SetSession(nil)
+ user.DeleteSession()
jsonResponse(w, http.StatusOK, Response{true, "Session information purged"})
-}
-
-func (prov *ProvisioningAPI) DeleteConnection(w http.ResponseWriter, r *http.Request) {
- user := r.Context().Value("user").(*User)
- if user.Conn == nil {
- jsonResponse(w, http.StatusNotFound, Error{
- Error: "You don't have a WhatsApp connection.",
- ErrCode: "not connected",
- })
- return
- }
- user.DeleteConnection()
- jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp and connection deleted"})
+ user.removeFromJIDMap(StateLoggedOut)
}
func (prov *ProvisioningAPI) Disconnect(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
- if user.Conn == nil {
+ if user.Client == nil {
jsonResponse(w, http.StatusNotFound, Error{
Error: "You don't have a WhatsApp connection.",
ErrCode: "no connection",
})
return
}
- err := user.Conn.Disconnect()
- if err == whatsapp.ErrNotConnected {
- jsonResponse(w, http.StatusNotFound, Error{
- Error: "You were not connected",
- ErrCode: "not connected",
- })
- return
- } else if err != nil {
- user.log.Warnln("Error while disconnecting:", err)
- jsonResponse(w, http.StatusInternalServerError, Error{
- Error: fmt.Sprintf("Unknown error while disconnecting: %v", err),
- ErrCode: err.Error(),
- })
- return
- }
- user.bridge.Metrics.TrackConnectionState(user.JID, false)
+ user.DeleteConnection()
jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp"})
+ user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
}
func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
- if user.Conn == nil {
+ if user.Client == nil {
if user.Session == nil {
jsonResponse(w, http.StatusForbidden, Error{
Error: "No existing connection and no session. Please log in first.",
ErrCode: "no session",
})
} else {
- user.Connect(false)
- jsonResponse(w, http.StatusOK, Response{true, "Created connection to WhatsApp."})
+ user.Connect()
+ jsonResponse(w, http.StatusAccepted, Response{true, "Created connection to WhatsApp."})
}
- return
- }
-
- user.log.Debugln("Received /reconnect request, disconnecting")
- wasConnected := true
- err := user.Conn.Disconnect()
- if err == whatsapp.ErrNotConnected {
- wasConnected = false
- } else if err != nil {
- user.log.Warnln("Error while disconnecting:", err)
- }
-
- user.log.Debugln("Restoring session for /reconnect")
- err = user.Conn.Restore(true, r.Context())
- user.log.Debugfln("Restore session for /reconnect responded with %v", err)
- if err == whatsapp.ErrInvalidSession {
- if user.Session != nil {
- user.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
- user.Conn.SetSession(*user.Session)
- err = user.Conn.Restore(true, r.Context())
- } else {
- jsonResponse(w, http.StatusForbidden, Error{
- Error: "You're not logged in",
- ErrCode: "not logged in",
- })
- return
- }
- }
- if err == whatsapp.ErrLoginInProgress {
- jsonResponse(w, http.StatusConflict, Error{
- Error: "A login or reconnection is already in progress.",
- ErrCode: "login in progress",
- })
- return
- } else if err == whatsapp.ErrAlreadyLoggedIn {
- jsonResponse(w, http.StatusConflict, Error{
- Error: "You were already connected.",
- ErrCode: err.Error(),
- })
- return
- }
- if err != nil {
- user.log.Warnln("Error while reconnecting:", err)
- jsonResponse(w, http.StatusInternalServerError, Error{
- Error: fmt.Sprintf("Unknown error while reconnecting: %v", err),
- ErrCode: err.Error(),
- })
- user.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
- err = user.Conn.Disconnect()
- if err != nil {
- user.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
- }
- return
- }
- user.ConnectionErrors = 0
- user.PostLogin()
-
- var msg string
- if wasConnected {
- msg = "Reconnected successfully."
} else {
- msg = "Connected successfully."
+ user.DeleteConnection()
+ user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WANotConnected})
+ user.Connect()
+ jsonResponse(w, http.StatusAccepted, Response{true, "Restarted connection to WhatsApp"})
}
-
- jsonResponse(w, http.StatusOK, Response{true, msg})
}
func (prov *ProvisioningAPI) Ping(w http.ResponseWriter, r *http.Request) {
@@ -259,39 +176,23 @@ func (prov *ProvisioningAPI) Ping(w http.ResponseWriter, r *http.Request) {
wa := map[string]interface{}{
"has_session": user.Session != nil,
"management_room": user.ManagementRoom,
- "jid": user.JID,
"conn": nil,
- "ping": nil,
}
- if user.Conn != nil {
+ if user.JID.IsEmpty() {
+ wa["jid"] = user.JID.String()
+ }
+ if user.Client != nil {
wa["conn"] = map[string]interface{}{
- "is_connected": user.Conn.IsConnected(),
- "is_logged_in": user.Conn.IsLoggedIn(),
- "is_login_in_progress": user.Conn.IsLoginInProgress(),
+ "is_connected": user.Client.IsConnected(),
+ "is_logged_in": user.Client.IsLoggedIn,
}
- user.log.Debugln("Pinging WhatsApp mobile due to /ping API request")
- err := user.Conn.AdminTest()
- var errStr string
- if err == whatsapp.ErrPingFalse {
- user.log.Debugln("Forwarding ping false error from provisioning API to HandleError")
- go user.HandleError(err)
- }
- if err != nil {
- errStr = err.Error()
- }
- wa["ping"] = map[string]interface{}{
- "ok": err == nil,
- "err": errStr,
- }
- user.log.Debugfln("Admin test response for /ping: %v (conn: %t, login: %t, in progress: %t)",
- err, user.Conn.IsConnected(), user.Conn.IsLoggedIn(), user.Conn.IsLoginInProgress())
}
resp := map[string]interface{}{
- "mxid": user.MXID,
- "admin": user.Admin,
- "whitelisted": user.Whitelisted,
- "relaybot_whitelisted": user.RelaybotWhitelisted,
- "whatsapp": wa,
+ "mxid": user.MXID,
+ "admin": user.Admin,
+ "whitelisted": user.Whitelisted,
+ "relay_whitelisted": user.RelayWhitelisted,
+ "whatsapp": wa,
}
jsonResponse(w, http.StatusOK, resp)
}
@@ -314,7 +215,7 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
force := strings.ToLower(r.URL.Query().Get("force")) != "false"
- if user.Conn == nil {
+ if user.Client == nil {
if !force {
jsonResponse(w, http.StatusNotFound, Error{
Error: "You're not connected",
@@ -322,7 +223,7 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
})
}
} else {
- err := user.Conn.Logout()
+ err := user.Client.Logout()
if err != nil {
user.log.Warnln("Error while logging out:", err)
if !force {
@@ -332,16 +233,15 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
})
return
}
+ } else {
+ user.Session = nil
}
user.DeleteConnection()
}
user.bridge.Metrics.TrackConnectionState(user.JID, false)
user.removeFromJIDMap(StateLoggedOut)
-
- // TODO this causes a foreign key violation, which should be fixed
- //ce.User.JID = ""
- user.SetSession(nil)
+ user.DeleteSession()
jsonResponse(w, http.StatusOK, Response{true, "Logged out successfully."})
}
@@ -361,26 +261,10 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
prov.log.Errorln("Failed to upgrade connection to websocket:", err)
return
}
- defer c.Close()
-
- if !user.Connect(true) {
- user.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.")
- _ = c.WriteJSON(Error{
- Error: "Failed to connect to WhatsApp",
- ErrCode: "connection error",
- })
- return
- }
-
- qrChan := make(chan string, 3)
- go func() {
- for code := range qrChan {
- if code == "stop" {
- return
- }
- _ = c.WriteJSON(map[string]interface{}{
- "code": code,
- })
+ defer func() {
+ err := c.Close()
+ if err != nil {
+ user.log.Debugln("Error closing websocket:", err)
}
}()
@@ -400,40 +284,63 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
return nil
})
- user.log.Debugln("Starting login via provisioning API")
- session, jid, err := user.Conn.Login(qrChan, ctx)
- qrChan <- "stop"
+ qrChan, err := user.Login(ctx)
if err != nil {
- var msg string
- if errors.Is(err, whatsapp.ErrAlreadyLoggedIn) {
- msg = "You're already logged in"
- } else if errors.Is(err, whatsapp.ErrLoginInProgress) {
- msg = "You have a login in progress already."
- } else if errors.Is(err, whatsapp.ErrLoginTimedOut) {
- msg = "QR code scan timed out. Please try again."
- } else if errors.Is(err, whatsapp.ErrInvalidWebsocket) {
- msg = "WhatsApp connection error. Please try again."
- // TODO might need to make sure it reconnects?
- } else if errors.Is(err, whatsapp.ErrMultiDeviceNotSupported) {
- msg = "WhatsApp multi-device is not currently supported. Please disable it and try again."
+ user.log.Errorf("Failed to log in from provisioning API:", err)
+ if errors.Is(err, ErrAlreadyLoggedIn) {
+ go user.Connect()
+ _ = c.WriteJSON(Error{
+ Error: "You're already logged into WhatsApp",
+ ErrCode: "already logged in",
+ })
} else {
- msg = fmt.Sprintf("Unknown error while logging in: %v", err)
+ _ = c.WriteJSON(Error{
+ Error: "Failed to connect to WhatsApp",
+ ErrCode: "connection error",
+ })
+ }
+ }
+ user.log.Debugln("Started login via provisioning API")
+
+ for {
+ select {
+ case evt := <-qrChan:
+ switch evt {
+ case whatsmeow.QRChannelSuccess:
+ jid := user.Client.Store.ID
+ user.log.Debugln("Successful login as", jid, "via provisioning API")
+ _ = c.WriteJSON(map[string]interface{}{
+ "success": true,
+ "jid": jid,
+ "phone": fmt.Sprintf("+%s", jid.User),
+ })
+ case whatsmeow.QRChannelTimeout:
+ user.log.Debugln("Login via provisioning API timed out")
+ _ = c.WriteJSON(Error{
+ Error: "QR code scan timed out. Please try again.",
+ ErrCode: "login timed out",
+ })
+ case whatsmeow.QRChannelErrUnexpectedEvent:
+ user.log.Debugln("Login via provisioning API failed due to unexpected event")
+ _ = c.WriteJSON(Error{
+ Error: "Got unexpected event while waiting for QRs, perhaps you're already logged in?",
+ ErrCode: "unexpected event",
+ })
+ case whatsmeow.QRChannelScannedWithoutMultidevice:
+ _ = c.WriteJSON(Error{
+ Error: "Please enable the WhatsApp multidevice beta and scan the QR code again.",
+ ErrCode: "multidevice not enabled",
+ })
+ continue
+ default:
+ _ = c.WriteJSON(map[string]interface{}{
+ "code": string(evt),
+ })
+ continue
+ }
+ return
+ case <-ctx.Done():
+ return
}
- user.log.Warnln("Failed to log in:", err)
- _ = c.WriteJSON(Error{
- Error: msg,
- ErrCode: err.Error(),
- })
- return
}
- user.log.Debugln("Successful login as", jid, "via provisioning API")
- user.ConnectionErrors = 0
- user.JID = strings.Replace(jid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, 1)
- user.addToJIDMap()
- user.SetSession(&session)
- _ = c.WriteJSON(map[string]interface{}{
- "success": true,
- "jid": user.JID,
- })
- user.PostLogin()
}
diff --git a/puppet.go b/puppet.go
index 557ef9a..4f82ed6 100644
--- a/puppet.go
+++ b/puppet.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -17,15 +17,19 @@
package main
import (
+ "errors"
"fmt"
+ "io"
"net/http"
"regexp"
- "strings"
"sync"
+ "time"
- "github.com/Rhymen/go-whatsapp"
+ "go.mau.fi/whatsmeow"
+ "go.mau.fi/whatsmeow/types"
log "maunium.net/go/maulogger/v2"
+
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/id"
@@ -34,19 +38,18 @@ import (
var userIDRegex *regexp.Regexp
-func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (whatsapp.JID, bool) {
+func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (jid types.JID, ok bool) {
if userIDRegex == nil {
userIDRegex = regexp.MustCompile(fmt.Sprintf("^@%s:%s$",
bridge.Config.Bridge.FormatUsername("([0-9]+)"),
bridge.Config.Homeserver.Domain))
}
match := userIDRegex.FindStringSubmatch(string(mxid))
- if match == nil || len(match) != 2 {
- return "", false
+ if len(match) == 2 {
+ jid = types.NewJID(match[1], types.DefaultUserServer)
+ ok = true
}
-
- jid := whatsapp.JID(match[1] + whatsapp.NewUserSuffix)
- return jid, true
+ return
}
func (bridge *Bridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
@@ -58,7 +61,13 @@ func (bridge *Bridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
return bridge.GetPuppetByJID(jid)
}
-func (bridge *Bridge) GetPuppetByJID(jid whatsapp.JID) *Puppet {
+func (bridge *Bridge) GetPuppetByJID(jid types.JID) *Puppet {
+ jid = jid.ToNonAD()
+ if jid.Server == types.LegacyUserServer {
+ jid.Server = types.DefaultUserServer
+ } else if jid.Server != types.DefaultUserServer {
+ return nil
+ }
bridge.puppetsLock.Lock()
defer bridge.puppetsLock.Unlock()
puppet, ok := bridge.puppets[jid]
@@ -123,12 +132,9 @@ func (bridge *Bridge) dbPuppetsToPuppets(dbPuppets []*database.Puppet) []*Puppet
return output
}
-func (bridge *Bridge) FormatPuppetMXID(jid whatsapp.JID) id.UserID {
+func (bridge *Bridge) FormatPuppetMXID(jid types.JID) id.UserID {
return id.NewUserID(
- bridge.Config.Bridge.FormatUsername(
- strings.Replace(
- jid,
- whatsapp.NewUserSuffix, "", 1)),
+ bridge.Config.Bridge.FormatUsername(jid.User),
bridge.Config.Homeserver.Domain)
}
@@ -149,7 +155,7 @@ type Puppet struct {
log log.Logger
typingIn id.RoomID
- typingAt int64
+ typingAt time.Time
MXID id.UserID
@@ -160,14 +166,8 @@ type Puppet struct {
syncLock sync.Mutex
}
-func (puppet *Puppet) PhoneNumber() string {
- return strings.Replace(puppet.JID, whatsapp.NewUserSuffix, "", 1)
-}
-
func (puppet *Puppet) IntentFor(portal *Portal) *appservice.IntentAPI {
- if (!portal.IsPrivateChat() && puppet.customIntent == nil) ||
- (portal.backfilling && portal.bridge.Config.Bridge.InviteOwnPuppetForBackfilling) ||
- portal.Key.JID == puppet.JID {
+ if (!portal.IsPrivateChat() && puppet.customIntent == nil) || portal.Key.JID == puppet.JID {
return puppet.DefaultIntent()
}
return puppet.customIntent
@@ -181,63 +181,64 @@ func (puppet *Puppet) DefaultIntent() *appservice.IntentAPI {
return puppet.bridge.AS.Intent(puppet.MXID)
}
-func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsapp.ProfilePicInfo) bool {
- if avatar == nil {
- var err error
- avatar, err = source.Conn.GetProfilePicThumb(puppet.JID)
- if err != nil {
- puppet.log.Warnln("Failed to get avatar:", err)
- return false
- }
- }
-
- if avatar.Status == 404 {
- avatar.Tag = "remove"
- avatar.Status = 0
- } else if avatar.Status == 401 && puppet.Avatar != "unauthorized" {
- puppet.Avatar = "unauthorized"
- return true
- }
- if avatar.Status != 0 || avatar.Tag == puppet.Avatar {
- return false
- }
-
- if avatar.Tag == "remove" || len(avatar.URL) == 0 {
- err := puppet.DefaultIntent().SetAvatarURL(id.ContentURI{})
- if err != nil {
- puppet.log.Warnln("Failed to remove avatar:", err)
- }
- puppet.AvatarURL = id.ContentURI{}
- puppet.Avatar = avatar.Tag
- go puppet.updatePortalAvatar()
- return true
- }
-
- data, err := avatar.DownloadBytes()
+func reuploadAvatar(intent *appservice.IntentAPI, url string) (id.ContentURI, error) {
+ getResp, err := http.DefaultClient.Get(url)
if err != nil {
- puppet.log.Warnln("Failed to download avatar:", err)
- return false
+ return id.ContentURI{}, fmt.Errorf("failed to download avatar: %w", err)
+ }
+ data, err := io.ReadAll(getResp.Body)
+ _ = getResp.Body.Close()
+ if err != nil {
+ return id.ContentURI{}, fmt.Errorf("failed to read avatar bytes: %w", err)
}
mime := http.DetectContentType(data)
- resp, err := puppet.DefaultIntent().UploadBytes(data, mime)
+ resp, err := intent.UploadBytes(data, mime)
if err != nil {
- puppet.log.Warnln("Failed to upload avatar:", err)
+ return id.ContentURI{}, fmt.Errorf("failed to upload avatar to Matrix: %w", err)
+ }
+ return resp.ContentURI, nil
+}
+
+func (puppet *Puppet) UpdateAvatar(source *User) bool {
+ avatar, err := source.Client.GetProfilePictureInfo(puppet.JID, false)
+ if err != nil {
+ if !errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) {
+ puppet.log.Warnln("Failed to get avatar URL:", err)
+ }
return false
+ } else if avatar == nil {
+ if puppet.Avatar == "remove" {
+ return false
+ }
+ puppet.AvatarURL = id.ContentURI{}
+ avatar = &types.ProfilePictureInfo{ID: "remove"}
+ } else if avatar.ID == puppet.Avatar {
+ return false
+ } else if len(avatar.URL) == 0 {
+ puppet.log.Warnln("Didn't get URL in response to avatar query")
+ return false
+ } else {
+ url, err := reuploadAvatar(puppet.DefaultIntent(), avatar.URL)
+ if err != nil {
+ puppet.log.Warnln("Failed to reupload avatar:", err)
+ return false
+ }
+
+ puppet.AvatarURL = url
}
- puppet.AvatarURL = resp.ContentURI
err = puppet.DefaultIntent().SetAvatarURL(puppet.AvatarURL)
if err != nil {
puppet.log.Warnln("Failed to set avatar:", err)
}
- puppet.Avatar = avatar.Tag
+ puppet.Avatar = avatar.ID
go puppet.updatePortalAvatar()
return true
}
-func (puppet *Puppet) UpdateName(source *User, contact whatsapp.Contact) bool {
- newName, quality := puppet.bridge.Config.Bridge.FormatDisplayname(contact)
+func (puppet *Puppet) UpdateName(source *User, contact types.ContactInfo) bool {
+ newName, quality := puppet.bridge.Config.Bridge.FormatDisplayname(puppet.JID, contact)
if puppet.Displayname != newName && quality >= puppet.NameQuality {
err := puppet.DefaultIntent().SetDisplayName(newName)
if err == nil {
@@ -288,25 +289,21 @@ func (puppet *Puppet) updatePortalName() {
})
}
-func (puppet *Puppet) SyncContactIfNecessary(source *User) {
- if len(puppet.Displayname) > 0 {
+func (puppet *Puppet) SyncContact(source *User, onlyIfNoName bool) {
+ if onlyIfNoName && len(puppet.Displayname) > 0 {
return
}
- source.Conn.Store.ContactsLock.RLock()
- contact, ok := source.Conn.Store.Contacts[puppet.JID]
- source.Conn.Store.ContactsLock.RUnlock()
- if !ok {
- puppet.log.Warnfln("No contact info found through %s in SyncContactIfNecessary", source.MXID)
- contact.JID = puppet.JID
- // Sync anyway to set a phone number name
- } else {
- puppet.log.Debugfln("Syncing contact info through %s / %s because puppet has no displayname", source.MXID, source.JID)
+ contact, err := source.Client.Store.Contacts.GetContact(puppet.JID)
+ if err != nil {
+ puppet.log.Warnfln("Failed to get contact info through %s in SyncContact: %v", source.MXID)
+ } else if !contact.Found {
+ puppet.log.Warnfln("No contact info found through %s in SyncContact", source.MXID)
}
puppet.Sync(source, contact)
}
-func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) {
+func (puppet *Puppet) Sync(source *User, contact types.ContactInfo) {
puppet.syncLock.Lock()
defer puppet.syncLock.Unlock()
err := puppet.DefaultIntent().EnsureRegistered()
@@ -314,15 +311,14 @@ func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) {
puppet.log.Errorln("Failed to ensure registered:", err)
}
- if contact.JID == source.JID {
- contact.Notify = source.pushName
+ if puppet.JID.User == source.JID.User {
+ contact.PushName = source.Client.Store.PushName
}
update := false
update = puppet.UpdateName(source, contact) || update
- // TODO figure out how to update avatars after being offline
if len(puppet.Avatar) == 0 || puppet.bridge.Config.Bridge.UserAvatarSync {
- update = puppet.UpdateAvatar(source, nil) || update
+ update = puppet.UpdateAvatar(source) || update
}
if update {
puppet.Update()
diff --git a/user.go b/user.go
index 9f0e16b..7778eab 100644
--- a/user.go
+++ b/user.go
@@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
-// Copyright (C) 2020 Tulir Asokan
+// Copyright (C) 2021 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
@@ -22,21 +22,21 @@ import (
"errors"
"fmt"
"net/http"
- "sort"
- "strings"
"sync"
- "sync/atomic"
"time"
- "github.com/skip2/go-qrcode"
log "maunium.net/go/maulogger/v2"
+ "go.mau.fi/whatsmeow/appstate"
+ waProto "go.mau.fi/whatsmeow/binary/proto"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/pushrules"
- "github.com/Rhymen/go-whatsapp"
- waBinary "github.com/Rhymen/go-whatsapp/binary"
- waProto "github.com/Rhymen/go-whatsapp/binary/proto"
+ "go.mau.fi/whatsmeow"
+ "go.mau.fi/whatsmeow/store"
+ "go.mau.fi/whatsmeow/types"
+ "go.mau.fi/whatsmeow/types/events"
+ waLog "go.mau.fi/whatsmeow/util/log"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
@@ -48,38 +48,20 @@ import (
type User struct {
*database.User
- Conn *whatsapp.Conn
+ Client *whatsmeow.Client
+ Session *store.Device
bridge *Bridge
log log.Logger
- Admin bool
- Whitelisted bool
- RelaybotWhitelisted bool
+ Admin bool
+ Whitelisted bool
+ RelayWhitelisted bool
- IsRelaybot bool
+ mgmtCreateLock sync.Mutex
+ connLock sync.Mutex
- ConnectionErrors int
- CommunityID string
-
- cleanDisconnection bool
- batteryWarningsSent int
- lastReconnection int64
- pushName string
-
- chatListReceived chan struct{}
- syncPortalsDone chan struct{}
-
- messageInput chan PortalMessage
- messageOutput chan PortalMessage
-
- syncStart chan struct{}
- syncWait sync.WaitGroup
- syncing int32
-
- mgmtCreateLock sync.Mutex
- connLock sync.Mutex
- cancelReconnect func()
+ historySyncs chan *events.HistorySync
prevBridgeStatus *BridgeState
}
@@ -98,27 +80,27 @@ func (bridge *Bridge) GetUserByMXID(userID id.UserID) *User {
return user
}
-func (bridge *Bridge) GetUserByJID(userID whatsapp.JID) *User {
+func (bridge *Bridge) GetUserByJID(jid types.JID) *User {
bridge.usersLock.Lock()
defer bridge.usersLock.Unlock()
- user, ok := bridge.usersByJID[userID]
+ user, ok := bridge.usersByUsername[jid.User]
if !ok {
- return bridge.loadDBUser(bridge.DB.User.GetByJID(userID), nil)
+ return bridge.loadDBUser(bridge.DB.User.GetByUsername(jid.User), nil)
}
return user
}
func (user *User) addToJIDMap() {
user.bridge.usersLock.Lock()
- user.bridge.usersByJID[user.JID] = user
+ user.bridge.usersByUsername[user.JID.User] = user
user.bridge.usersLock.Unlock()
}
func (user *User) removeFromJIDMap(state BridgeStateEvent) {
user.bridge.usersLock.Lock()
- jidUser, ok := user.bridge.usersByJID[user.JID]
+ jidUser, ok := user.bridge.usersByUsername[user.JID.User]
if ok && user == jidUser {
- delete(user.bridge.usersByJID, user.JID)
+ delete(user.bridge.usersByUsername, user.JID.User)
}
user.bridge.usersLock.Unlock()
user.bridge.Metrics.TrackLoginState(user.JID, false)
@@ -151,8 +133,18 @@ func (bridge *Bridge) loadDBUser(dbUser *database.User, mxid *id.UserID) *User {
}
user := bridge.NewUser(dbUser)
bridge.usersByMXID[user.MXID] = user
- if len(user.JID) > 0 {
- bridge.usersByJID[user.JID] = user
+ if !user.JID.IsEmpty() {
+ var err error
+ user.Session, err = bridge.WAContainer.GetDevice(user.JID)
+ if err != nil {
+ user.log.Errorfln("Failed to load user's whatsapp session: %v", err)
+ } else if user.Session == nil {
+ user.log.Warnfln("Didn't find session data for %s, treating user as logged out", user.JID)
+ user.JID = types.EmptyJID
+ user.Update()
+ } else {
+ bridge.usersByUsername[user.JID.User] = user
+ }
}
if len(user.ManagementRoom) > 0 {
bridge.managementRooms[user.ManagementRoom] = user
@@ -160,41 +152,25 @@ func (bridge *Bridge) loadDBUser(dbUser *database.User, mxid *id.UserID) *User {
return user
}
-func (user *User) GetPortals() []*Portal {
- keys := user.User.GetPortalKeys()
- portals := make([]*Portal, len(keys))
-
- user.bridge.portalsLock.Lock()
- for i, key := range keys {
- portal, ok := user.bridge.portalsByJID[key]
- if !ok {
- portal = user.bridge.loadDBPortal(user.bridge.DB.Portal.GetByJID(key), &key)
- }
- portals[i] = portal
- }
- user.bridge.portalsLock.Unlock()
- return portals
-}
-
func (bridge *Bridge) NewUser(dbUser *database.User) *User {
user := &User{
User: dbUser,
bridge: bridge,
log: bridge.Log.Sub("User").Sub(string(dbUser.MXID)),
- IsRelaybot: false,
-
- chatListReceived: make(chan struct{}, 1),
- syncPortalsDone: make(chan struct{}, 1),
- syncStart: make(chan struct{}, 1),
- messageInput: make(chan PortalMessage),
- messageOutput: make(chan PortalMessage, bridge.Config.Bridge.UserMessageBuffer),
+ historySyncs: make(chan *events.HistorySync, 32),
}
- user.RelaybotWhitelisted = user.bridge.Config.Bridge.Permissions.IsRelaybotWhitelisted(user.MXID)
+ user.RelayWhitelisted = user.bridge.Config.Bridge.Permissions.IsRelayWhitelisted(user.MXID)
user.Whitelisted = user.bridge.Config.Bridge.Permissions.IsWhitelisted(user.MXID)
user.Admin = user.bridge.Config.Bridge.Permissions.IsAdmin(user.MXID)
- go user.handleMessageLoop()
- go user.runMessageRingBuffer()
+ go func() {
+ for evt := range user.historySyncs {
+ if evt == nil {
+ return
+ }
+ user.handleHistorySync(evt.Data)
+ }
+ }()
return user
}
@@ -230,255 +206,99 @@ func (user *User) SetManagementRoom(roomID id.RoomID) {
user.Update()
}
-func (user *User) SetSession(session *whatsapp.Session) {
- if session == nil {
- user.Session = nil
- user.LastConnection = 0
- } else if len(session.Wid) > 0 {
- user.Session = session
- } else {
- return
+type waLogger struct{ l log.Logger }
+
+func (w *waLogger) Debugf(msg string, args ...interface{}) { w.l.Debugfln(msg, args...) }
+func (w *waLogger) Infof(msg string, args ...interface{}) { w.l.Infofln(msg, args...) }
+func (w *waLogger) Warnf(msg string, args ...interface{}) { w.l.Warnfln(msg, args...) }
+func (w *waLogger) Errorf(msg string, args ...interface{}) { w.l.Errorfln(msg, args...) }
+func (w *waLogger) Sub(module string) waLog.Logger { return &waLogger{l: w.l.Sub(module)} }
+
+var ErrAlreadyLoggedIn = errors.New("already logged in")
+
+func (user *User) Login(ctx context.Context) (<-chan whatsmeow.QRChannelItem, error) {
+ user.connLock.Lock()
+ defer user.connLock.Unlock()
+ if user.Session != nil {
+ return nil, ErrAlreadyLoggedIn
+ } else if user.Client != nil {
+ user.unlockedDeleteConnection()
}
- user.Update()
+ newSession := user.bridge.WAContainer.NewDevice()
+ newSession.Log = &waLogger{user.log.Sub("Session")}
+ user.Client = whatsmeow.NewClient(newSession, &waLogger{user.log.Sub("Client")})
+ user.Client.AddEventHandler(user.HandleEvent)
+ qrChan, err := user.Client.GetQRChannel(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get QR channel: %w", err)
+ }
+ err = user.Client.Connect()
+ if err != nil {
+ return nil, fmt.Errorf("failed to connect to WhatsApp: %w", err)
+ }
+ return qrChan, nil
}
-func (user *User) Connect(evenIfNoSession bool) bool {
+func (user *User) Connect() bool {
user.connLock.Lock()
- if user.Conn != nil {
- user.connLock.Unlock()
- if user.Conn.IsConnected() {
- return true
- } else {
- return user.RestoreSession()
- }
- } else if !evenIfNoSession && user.Session == nil {
- user.connLock.Unlock()
+ defer user.connLock.Unlock()
+ if user.Client != nil {
+ return user.Client.IsConnected()
+ } else if user.Session == nil {
return false
}
user.log.Debugln("Connecting to WhatsApp")
- if user.Session != nil {
- user.sendBridgeState(BridgeState{StateEvent: StateConnecting, Error: WAConnecting})
+ user.sendBridgeState(BridgeState{StateEvent: StateConnecting, Error: WAConnecting})
+ user.Client = whatsmeow.NewClient(user.Session, &waLogger{user.log.Sub("Client")})
+ user.Client.AddEventHandler(user.HandleEvent)
+ err := user.Client.Connect()
+ if err != nil {
+ user.log.Warnln("Error connecting to WhatsApp:", err)
+ return false
}
- timeout := time.Duration(user.bridge.Config.Bridge.ConnectionTimeout)
- if timeout == 0 {
- timeout = 20
+ return true
+}
+
+func (user *User) unlockedDeleteConnection() {
+ if user.Client == nil {
+ return
}
- user.Conn = whatsapp.NewConn(&whatsapp.Options{
- Timeout: timeout * time.Second,
- LongClientName: user.bridge.Config.WhatsApp.OSName,
- ShortClientName: user.bridge.Config.WhatsApp.BrowserName,
- ClientVersion: WAVersion,
- Log: user.log.Sub("Conn"),
- Handler: []whatsapp.Handler{user},
- })
- user.setupAdminTestHooks()
- user.connLock.Unlock()
- return user.RestoreSession()
+ user.Client.Disconnect()
+ user.Client.RemoveEventHandlers()
+ user.Client = nil
+ user.bridge.Metrics.TrackConnectionState(user.JID, false)
}
func (user *User) DeleteConnection() {
user.connLock.Lock()
- if user.Conn == nil {
- user.connLock.Unlock()
- return
- }
- err := user.Conn.Disconnect()
- if err != nil && err != whatsapp.ErrNotConnected {
- user.log.Warnln("Error disconnecting: %v", err)
- }
- user.Conn.RemoveHandlers()
- user.Conn = nil
- user.bridge.Metrics.TrackConnectionState(user.JID, false)
- user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
- user.connLock.Unlock()
-}
-
-func (user *User) RestoreSession() bool {
- if user.Session != nil {
- user.Conn.SetSession(*user.Session)
- ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
- defer cancel()
- err := user.Conn.Restore(true, ctx)
- if err == whatsapp.ErrAlreadyLoggedIn {
- return true
- } else if err != nil {
- user.log.Errorln("Failed to restore session:", err)
- if errors.Is(err, whatsapp.ErrUnpaired) {
- user.sendMarkdownBridgeAlert("\u26a0 Failed to connect to WhatsApp: unpaired from phone. " +
- "To re-pair your phone, log in again.")
- user.removeFromJIDMap(StateBadCredentials)
- //user.JID = ""
- user.SetSession(nil)
- user.DeleteConnection()
- return false
- } else {
- user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
- user.sendMarkdownBridgeAlert("\u26a0 Failed to connect to WhatsApp. Make sure WhatsApp " +
- "on your phone is reachable and use `reconnect` to try connecting again.")
- }
- user.log.Debugln("Disconnecting due to failed session restore...")
- err = user.Conn.Disconnect()
- if err != nil {
- user.log.Errorln("Failed to disconnect after failed session restore:", err)
- }
- return false
- }
- user.ConnectionErrors = 0
- user.log.Debugln("Session restored successfully")
- user.PostLogin()
- }
- return true
+ defer user.connLock.Unlock()
+ user.unlockedDeleteConnection()
}
func (user *User) HasSession() bool {
return user.Session != nil
}
+func (user *User) DeleteSession() {
+ if user.Session != nil {
+ err := user.Session.Delete()
+ if err != nil {
+ user.log.Warnln("Failed to delete session:", err)
+ }
+ user.Session = nil
+ }
+ if !user.JID.IsEmpty() {
+ user.JID = types.EmptyJID
+ user.Update()
+ }
+}
+
func (user *User) IsConnected() bool {
- return user.Conn != nil && user.Conn.IsConnected() && user.Conn.IsLoggedIn()
+ return user.Client != nil && user.Client.IsConnected()
}
-func (user *User) IsLoginInProgress() bool {
- return user.Conn != nil && user.Conn.IsLoginInProgress()
-}
-
-func (user *User) loginQrChannel(ce *CommandEvent, qrChan <-chan string, eventIDChan chan<- id.EventID) {
- var qrEventID id.EventID
- for code := range qrChan {
- if code == "stop" {
- return
- }
- qrCode, err := qrcode.Encode(code, qrcode.Low, 256)
- if err != nil {
- user.log.Errorln("Failed to encode QR code:", err)
- ce.Reply("Failed to encode QR code: %v", err)
- return
- }
-
- bot := user.bridge.AS.BotClient()
-
- resp, err := bot.UploadBytes(qrCode, "image/png")
- if err != nil {
- user.log.Errorln("Failed to upload QR code:", err)
- ce.Reply("Failed to upload QR code: %v", err)
- return
- }
-
- if qrEventID == "" {
- sendResp, err := bot.SendImage(ce.RoomID, code, resp.ContentURI)
- if err != nil {
- user.log.Errorln("Failed to send QR code to user:", err)
- return
- }
- qrEventID = sendResp.EventID
- eventIDChan <- qrEventID
- } else {
- _, err = bot.SendMessageEvent(ce.RoomID, event.EventMessage, &event.MessageEventContent{
- MsgType: event.MsgImage,
- Body: code,
- URL: resp.ContentURI.CUString(),
- NewContent: &event.MessageEventContent{
- MsgType: event.MsgImage,
- Body: code,
- URL: resp.ContentURI.CUString(),
- },
- RelatesTo: &event.RelatesTo{
- Type: event.RelReplace,
- EventID: qrEventID,
- },
- })
- if err != nil {
- user.log.Errorln("Failed to send edited QR code to user:", err)
- }
- }
- }
-}
-
-func (user *User) Login(ce *CommandEvent) {
- qrChan := make(chan string, 3)
- eventIDChan := make(chan id.EventID, 1)
- go user.loginQrChannel(ce, qrChan, eventIDChan)
- session, jid, err := user.Conn.Login(qrChan, nil)
- qrChan <- "stop"
- if err != nil {
- var eventID id.EventID
- select {
- case eventID = <-eventIDChan:
- default:
- }
- reply := event.MessageEventContent{
- MsgType: event.MsgText,
- }
- if err == whatsapp.ErrAlreadyLoggedIn {
- reply.Body = "You're already logged in"
- } else if err == whatsapp.ErrLoginInProgress {
- reply.Body = "You have a login in progress already."
- } else if err == whatsapp.ErrLoginTimedOut {
- reply.Body = "QR code scan timed out. Please try again."
- } else if errors.Is(err, whatsapp.ErrMultiDeviceNotSupported) {
- reply.Body = "WhatsApp multi-device is not currently supported. Please disable it and try again."
- } else {
- user.log.Warnln("Failed to log in:", err)
- reply.Body = fmt.Sprintf("Unknown error while logging in: %v", err)
- }
- msg := reply
- if eventID != "" {
- msg.NewContent = &reply
- msg.RelatesTo = &event.RelatesTo{
- Type: event.RelReplace,
- EventID: eventID,
- }
- }
- _, _ = ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &msg)
- return
- }
- // TODO there's a bit of duplication between this and the provisioning API login method
- // Also between the two logout methods (commands.go and provisioning.go)
- user.log.Debugln("Successful login as", jid, "via command")
- user.ConnectionErrors = 0
- user.JID = strings.Replace(jid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, 1)
- user.addToJIDMap()
- user.SetSession(&session)
- ce.Reply("Successfully logged in, synchronizing chats...")
- user.PostLogin()
-}
-
-type Chat struct {
- whatsapp.Chat
- Portal *Portal
- Contact whatsapp.Contact
-}
-
-type ChatList []Chat
-
-func (cl ChatList) Len() int {
- return len(cl)
-}
-
-func (cl ChatList) Less(i, j int) bool {
- return cl[i].LastMessageTime > cl[j].LastMessageTime
-}
-
-func (cl ChatList) Swap(i, j int) {
- cl[i], cl[j] = cl[j], cl[i]
-}
-
-func (user *User) PostLogin() {
- user.sendBridgeState(BridgeState{StateEvent: StateBackfilling})
- user.bridge.Metrics.TrackConnectionState(user.JID, true)
- user.bridge.Metrics.TrackLoginState(user.JID, true)
- user.bridge.Metrics.TrackBufferLength(user.MXID, len(user.messageOutput))
- if !atomic.CompareAndSwapInt32(&user.syncing, 0, 1) {
- // TODO we should cleanly stop the old sync and start a new one instead of not starting a new one
- user.log.Warnln("There seems to be a post-sync already in progress, not starting a new one")
- return
- }
- user.log.Debugln("Locking processing of incoming messages and starting post-login sync")
- user.chatListReceived = make(chan struct{}, 1)
- user.syncPortalsDone = make(chan struct{}, 1)
- user.syncWait.Add(1)
- user.syncStart <- struct{}{}
- go user.intPostLogin()
+func (user *User) IsLoggedIn() bool {
+ return user.IsConnected() && user.Client.IsLoggedIn
}
func (user *User) tryAutomaticDoublePuppeting() {
@@ -522,172 +342,141 @@ func (user *User) sendMarkdownBridgeAlert(formatString string, args ...interface
}
}
-func (user *User) postConnPing() bool {
- user.log.Debugln("Making post-connection ping")
- var err error
- for i := 0; ; i++ {
- err = user.Conn.AdminTest()
- if err == nil {
- user.log.Debugln("Post-connection ping OK")
- return true
- } else if errors.Is(err, whatsapp.ErrConnectionTimeout) && i < 5 {
- user.log.Warnfln("Post-connection ping timed out, sending new one")
+func (user *User) handleHistorySync(evt *waProto.HistorySync) {
+ if evt.GetSyncType() != waProto.HistorySync_RECENT && evt.GetSyncType() != waProto.HistorySync_FULL {
+ return
+ }
+ user.log.Infofln("Handling history sync with type %s, chunk order %d, progress %d%%", evt.GetSyncType(), evt.GetChunkOrder(), evt.GetProgress())
+ for _, conv := range evt.GetConversations() {
+ jid, err := types.ParseJID(conv.GetId())
+ if err != nil {
+ user.log.Warnfln("Failed to parse chat JID '%s' in history sync: %v", conv.GetId(), err)
+ continue
+ }
+
+ muteEnd := time.Unix(int64(conv.GetMuteEndTime()), 0)
+ if muteEnd.After(time.Now()) {
+ _ = user.Client.Store.ChatSettings.PutMutedUntil(jid, muteEnd)
+ }
+ if conv.GetArchived() {
+ _ = user.Client.Store.ChatSettings.PutArchived(jid, true)
+ }
+ if conv.GetPinned() > 0 {
+ _ = user.Client.Store.ChatSettings.PutPinned(jid, true)
+ }
+
+ portal := user.GetPortalByJID(jid)
+ if user.bridge.Config.Bridge.HistorySync.CreatePortals && len(portal.MXID) == 0 {
+ user.log.Debugln("Creating portal for", portal.Key.JID, "as part of history sync handling")
+ err = portal.CreateMatrixRoom(user)
+ if err != nil {
+ user.log.Warnfln("Failed to create room for %s during backfill: %v", portal.Key.JID, err)
+ continue
+ }
+ }
+ if len(portal.MXID) == 0 {
+ user.log.Debugln("No room created, not bridging history sync payload for", portal.Key.JID)
+ } else if !user.bridge.Config.Bridge.HistorySync.Backfill {
+ user.log.Debugln("Backfill is disabled, not bridging history sync payload for", portal.Key.JID)
} else {
- break
+ user.log.Debugln("Bridging history sync payload for", portal.Key.JID)
+ portal.backfill(user, conv.GetMessages())
+ if !conv.GetMarkedAsUnread() && conv.GetUnreadCount() == 0 {
+ user.markSelfReadFull(portal)
+ }
}
}
- user.log.Errorfln("Post-connection ping failed: %v. Disconnecting and then reconnecting after a second", err)
- disconnectErr := user.Conn.Disconnect()
- if disconnectErr != nil {
- user.log.Warnln("Error while disconnecting after failed post-connection ping:", disconnectErr)
- }
- user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
- user.bridge.Metrics.TrackDisconnection(user.MXID)
- go func() {
- time.Sleep(1 * time.Second)
- user.tryReconnect(fmt.Sprintf("Post-connection ping failed: %v", err))
- }()
- return false
-}
-
-func (user *User) intPostLogin() {
- defer atomic.StoreInt32(&user.syncing, 0)
- defer user.syncWait.Done()
- user.lastReconnection = time.Now().Unix()
- user.createCommunity()
- user.tryAutomaticDoublePuppeting()
-
- user.log.Debugln("Waiting for chat list receive confirmation")
- select {
- case <-user.chatListReceived:
- user.log.Debugln("Chat list receive confirmation received in PostLogin")
- case <-time.After(time.Duration(user.bridge.Config.Bridge.ChatListWait) * time.Second):
- user.log.Warnln("Timed out waiting for chat list to arrive!")
- user.postConnPing()
- return
- }
-
- if !user.postConnPing() {
- user.log.Debugln("Post-connection ping failed, unlocking processing of incoming messages.")
- return
- }
-
- user.log.Debugln("Waiting for portal sync complete confirmation")
- select {
- case <-user.syncPortalsDone:
- user.log.Debugln("Post-connection portal sync complete, unlocking processing of incoming messages.")
- // TODO this is too short, maybe a per-portal duration?
- case <-time.After(time.Duration(user.bridge.Config.Bridge.PortalSyncWait) * time.Second):
- user.log.Warnln("Timed out waiting for portal sync to complete! Unlocking processing of incoming messages.")
- }
- user.sendBridgeState(BridgeState{StateEvent: StateConnected})
-}
-
-type NormalMessage interface {
- GetInfo() whatsapp.MessageInfo
}
func (user *User) HandleEvent(event interface{}) {
switch v := event.(type) {
- case NormalMessage:
- info := v.GetInfo()
- user.messageInput <- PortalMessage{info.RemoteJid, user, v, info.Timestamp}
- case whatsapp.MessageRevocation:
- user.messageInput <- PortalMessage{v.RemoteJid, user, v, 0}
- case whatsapp.StreamEvent:
- user.HandleStreamEvent(v)
- case []whatsapp.Chat:
- user.HandleChatList(v)
- case []whatsapp.Contact:
- user.HandleContactList(v)
- case error:
- user.HandleError(v)
- case whatsapp.Contact:
- go user.HandleNewContact(v)
- case whatsapp.BatteryMessage:
- user.HandleBatteryMessage(v)
- case whatsapp.CallInfo:
- user.HandleCallInfo(v)
- case whatsapp.PresenceEvent:
- go user.HandlePresence(v)
- case whatsapp.JSONMsgInfo:
- go user.HandleMsgInfo(v)
- case whatsapp.ReceivedMessage:
- user.HandleReceivedMessage(v)
- case whatsapp.ReadMessage:
- user.HandleReadMessage(v)
- case whatsapp.JSONCommand:
- user.HandleCommand(v)
- case whatsapp.ChatUpdate:
- user.HandleChatUpdate(v)
- case whatsapp.ConnInfo:
- user.HandleConnInfo(v)
- case whatsapp.MuteMessage:
- portal := user.bridge.GetPortalByJID(user.PortalKey(v.JID))
- if portal != nil {
- go user.updateChatMute(nil, portal, v.MutedUntil)
+ case *events.LoggedOut:
+ go user.handleLoggedOut(v.OnConnect)
+ user.bridge.Metrics.TrackConnectionState(user.JID, false)
+ user.bridge.Metrics.TrackLoginState(user.JID, false)
+ case *events.Connected:
+ go user.sendBridgeState(BridgeState{StateEvent: StateConnected})
+ user.bridge.Metrics.TrackConnectionState(user.JID, true)
+ user.bridge.Metrics.TrackLoginState(user.JID, true)
+ go func() {
+ err := user.Client.SendPresence(types.PresenceUnavailable)
+ if err != nil {
+ user.log.Warnln("Failed to send initial presence:", err)
+ }
+ }()
+ go user.tryAutomaticDoublePuppeting()
+ case *events.AppStateSyncComplete:
+ if len(user.Client.Store.PushName) > 0 && v.Name == appstate.WAPatchCriticalBlock {
+ err := user.Client.SendPresence(types.PresenceUnavailable)
+ if err != nil {
+ user.log.Warnln("Failed to send presence after app state sync:", err)
+ }
}
- case whatsapp.ArchiveMessage:
- portal := user.bridge.GetPortalByJID(user.PortalKey(v.JID))
- if portal != nil {
- go user.updateChatTag(nil, portal, user.bridge.Config.Bridge.ArchiveTag, v.IsArchived)
+ case *events.PushNameSetting:
+ // Send presence available when connecting and when the pushname is changed.
+ // This makes sure that outgoing messages always have the right pushname.
+ err := user.Client.SendPresence(types.PresenceUnavailable)
+ if err != nil {
+ user.log.Warnln("Failed to send presence after push name update:", err)
}
- case whatsapp.PinMessage:
- portal := user.bridge.GetPortalByJID(user.PortalKey(v.JID))
+ case *events.PairSuccess:
+ user.Session = user.Client.Store
+ user.JID = v.ID
+ user.addToJIDMap()
+ user.Update()
+ case *events.ConnectFailure, *events.StreamError:
+ go user.sendBridgeState(BridgeState{StateEvent: StateUnknownError})
+ user.bridge.Metrics.TrackConnectionState(user.JID, false)
+ case *events.Disconnected:
+ go user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect})
+ user.bridge.Metrics.TrackConnectionState(user.JID, false)
+ case *events.Contact:
+ go user.syncPuppet(v.JID)
+ case *events.PushName:
+ go user.syncPuppet(v.JID)
+ case *events.GroupInfo:
+ go user.handleGroupUpdate(v)
+ case *events.Picture:
+ go user.handlePictureUpdate(v)
+ case *events.Receipt:
+ go user.handleReceipt(v)
+ case *events.ChatPresence:
+ go user.handleChatPresence(v)
+ case *events.Message:
+ portal := user.GetPortalByJID(v.Info.Chat)
+ portal.messages <- PortalMessage{evt: v, source: user}
+ case *events.UndecryptableMessage:
+ portal := user.GetPortalByJID(v.Info.Chat)
+ portal.messages <- PortalMessage{undecryptable: v, source: user}
+ case *events.HistorySync:
+ user.historySyncs <- v
+ case *events.Mute:
+ portal := user.GetPortalByJID(v.JID)
if portal != nil {
- go user.updateChatTag(nil, portal, user.bridge.Config.Bridge.PinnedTag, v.IsPinned)
+ var mutedUntil time.Time
+ if v.Action.GetMuted() {
+ mutedUntil = time.Unix(v.Action.GetMuteEndTimestamp(), 0)
+ }
+ go user.updateChatMute(nil, portal, mutedUntil)
}
- case whatsapp.RawJSONMessage:
- user.HandleJSONMessage(v)
- case *waProto.WebMessageInfo:
- user.updateLastConnectionIfNecessary()
- // TODO trace log
- //user.log.Debugfln("WebMessageInfo: %+v", v)
- case *waBinary.Node:
- user.log.Debugfln("Unknown binary message: %+v", v)
+ case *events.Archive:
+ portal := user.GetPortalByJID(v.JID)
+ if portal != nil {
+ go user.updateChatTag(nil, portal, user.bridge.Config.Bridge.ArchiveTag, v.Action.GetArchived())
+ }
+ case *events.Pin:
+ portal := user.GetPortalByJID(v.JID)
+ if portal != nil {
+ go user.updateChatTag(nil, portal, user.bridge.Config.Bridge.PinnedTag, v.Action.GetPinned())
+ }
+ case *events.AppState:
+ // Ignore
default:
user.log.Debugfln("Unknown type of event in HandleEvent: %T", v)
}
}
-func (user *User) HandleStreamEvent(evt whatsapp.StreamEvent) {
- if evt.Type == whatsapp.StreamSleep {
- if user.lastReconnection+60 > time.Now().Unix() {
- user.lastReconnection = 0
- user.log.Infoln("Stream went to sleep soon after reconnection, making new post-connection ping in 20 seconds")
- go func() {
- time.Sleep(20 * time.Second)
- // TODO if this happens during the post-login sync, it can get stuck forever
- // TODO check if the above is still true
- user.postConnPing()
- }()
- }
- } else {
- user.log.Infofln("Stream event: %+v", evt)
- }
-}
-
-func (user *User) HandleChatList(chats []whatsapp.Chat) {
- user.log.Infoln("Chat list received")
- chatMap := make(map[string]whatsapp.Chat)
- user.Conn.Store.ChatsLock.RLock()
- for _, chat := range user.Conn.Store.Chats {
- chatMap[chat.JID] = chat
- }
- user.Conn.Store.ChatsLock.RUnlock()
- for _, chat := range chats {
- chatMap[chat.JID] = chat
- }
- select {
- case user.chatListReceived <- struct{}{}:
- user.log.Debugln("Sent chat list receive confirmation from HandleChatList")
- default:
- user.log.Debugln("Failed to send chat list receive confirmation from HandleChatList, channel probably full")
- }
- go user.syncPortals(chatMap, false)
-}
-
-func (user *User) updateChatMute(intent *appservice.IntentAPI, portal *Portal, mutedUntil int64) {
+func (user *User) updateChatMute(intent *appservice.IntentAPI, portal *Portal, mutedUntil time.Time) {
if len(portal.MXID) == 0 || !user.bridge.Config.Bridge.MuteBridging {
return
} else if intent == nil {
@@ -698,7 +487,7 @@ func (user *User) updateChatMute(intent *appservice.IntentAPI, portal *Portal, m
intent = doublePuppet.CustomIntent()
}
var err error
- if mutedUntil != -1 && mutedUntil < time.Now().Unix() {
+ if mutedUntil.IsZero() && mutedUntil.Before(time.Now()) {
user.log.Debugfln("Portal %s is muted until %d, unmuting...", portal.MXID, mutedUntil)
err = intent.DeletePushRule("global", pushrules.RoomRule, string(portal.MXID))
} else {
@@ -757,126 +546,30 @@ type CustomReadReceipt struct {
DoublePuppet bool `json:"net.maunium.whatsapp.puppet,omitempty"`
}
-func (user *User) syncChatDoublePuppetDetails(doublePuppet *Puppet, chat Chat, justCreated bool) {
- if doublePuppet == nil || doublePuppet.CustomIntent() == nil || len(chat.Portal.MXID) == 0 {
+func (user *User) syncChatDoublePuppetDetails(portal *Portal, justCreated bool) {
+ doublePuppet := portal.bridge.GetPuppetByCustomMXID(user.MXID)
+ if doublePuppet == nil {
return
}
- intent := doublePuppet.CustomIntent()
- if chat.UnreadCount == 0 && (justCreated || !user.bridge.Config.Bridge.MarkReadOnlyOnCreate) {
- lastMessage := user.bridge.DB.Message.GetLastInChatBefore(chat.Portal.Key, chat.ReceivedAt.Unix())
- if lastMessage != nil {
- err := intent.MarkReadWithContent(chat.Portal.MXID, lastMessage.MXID, &CustomReadReceipt{DoublePuppet: true})
- if err != nil {
- user.log.Warnfln("Failed to mark %s in %s as read after backfill: %v", lastMessage.MXID, chat.Portal.MXID, err)
- }
- }
- } else if chat.UnreadCount == -1 {
- user.log.Debugfln("Invalid unread count (missing field?) in chat info %+v", chat.Source)
+ if doublePuppet == nil || doublePuppet.CustomIntent() == nil || len(portal.MXID) == 0 {
+ return
}
if justCreated || !user.bridge.Config.Bridge.TagOnlyOnCreate {
- user.updateChatMute(intent, chat.Portal, chat.MutedUntil)
- user.updateChatTag(intent, chat.Portal, user.bridge.Config.Bridge.ArchiveTag, chat.IsArchived)
- user.updateChatTag(intent, chat.Portal, user.bridge.Config.Bridge.PinnedTag, chat.IsPinned)
- }
-}
-
-func (user *User) syncPortal(chat Chat) {
- // Don't sync unless chat meta sync is enabled or portal doesn't exist
- if user.bridge.Config.Bridge.ChatMetaSync || len(chat.Portal.MXID) == 0 {
- failedToCreate := chat.Portal.Sync(user, chat.Contact)
- if failedToCreate {
+ chat, err := user.Client.Store.ChatSettings.GetChatSettings(portal.Key.JID)
+ if err != nil {
+ user.log.Warnfln("Failed to get settings of %s: %v", portal.Key.JID, err)
return
}
- }
- err := chat.Portal.BackfillHistory(user, chat.LastMessageTime)
- if err != nil {
- chat.Portal.log.Errorln("Error backfilling history:", err)
- }
-}
-
-func (user *User) collectChatList(chatMap map[string]whatsapp.Chat) ChatList {
- if chatMap == nil {
- chatMap = user.Conn.Store.Chats
- }
- user.log.Infoln("Reading chat list")
- chats := make(ChatList, 0, len(chatMap))
- existingKeys := user.GetInCommunityMap()
- portalKeys := make([]database.PortalKeyWithMeta, 0, len(chatMap))
- for _, chat := range chatMap {
- portal := user.GetPortalByJID(chat.JID)
-
- user.Conn.Store.ContactsLock.RLock()
- contact, _ := user.Conn.Store.Contacts[chat.JID]
- user.Conn.Store.ContactsLock.RUnlock()
- chats = append(chats, Chat{
- Chat: chat,
- Portal: portal,
- Contact: contact,
- })
- var inCommunity, ok bool
- if inCommunity, ok = existingKeys[portal.Key]; !ok || !inCommunity {
- inCommunity = user.addPortalToCommunity(portal)
- if portal.IsPrivateChat() {
- puppet := user.bridge.GetPuppetByJID(portal.Key.JID)
- user.addPuppetToCommunity(puppet)
- }
- }
- portalKeys = append(portalKeys, database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity})
- }
- user.log.Infoln("Read chat list, updating user-portal mapping")
- err := user.SetPortalKeys(portalKeys)
- if err != nil {
- user.log.Warnln("Failed to update user-portal mapping:", err)
- }
- sort.Sort(chats)
- return chats
-}
-
-func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool) {
- // TODO use contexts instead of checking if user.Conn is the same?
- connAtStart := user.Conn
-
- chats := user.collectChatList(chatMap)
-
- limit := user.bridge.Config.Bridge.InitialChatSync
- if limit < 0 {
- limit = len(chats)
- }
- if user.Conn != connAtStart {
- user.log.Debugln("Connection seems to have changed before sync, cancelling")
- return
- }
- now := time.Now().Unix()
- user.log.Infoln("Syncing portals")
- doublePuppet := user.bridge.GetPuppetByCustomMXID(user.MXID)
- for i, chat := range chats {
- if chat.LastMessageTime+user.bridge.Config.Bridge.SyncChatMaxAge < now {
- break
- }
- create := (chat.LastMessageTime >= user.LastConnection && user.LastConnection > 0) || i < limit
- if len(chat.Portal.MXID) > 0 || create || createAll {
- user.log.Debugfln("Syncing chat %+v", chat.Chat.Source)
- justCreated := len(chat.Portal.MXID) == 0
- user.syncPortal(chat)
- user.syncChatDoublePuppetDetails(doublePuppet, chat, justCreated)
- }
- }
- if user.Conn != connAtStart {
- user.log.Debugln("Connection seems to have changed during sync, cancelling")
- return
- }
- user.UpdateDirectChats(nil)
-
- user.log.Infoln("Finished syncing portals")
- select {
- case user.syncPortalsDone <- struct{}{}:
- default:
+ intent := doublePuppet.CustomIntent()
+ user.updateChatMute(intent, portal, chat.MutedUntil)
+ user.updateChatTag(intent, portal, user.bridge.Config.Bridge.ArchiveTag, chat.Archived)
+ user.updateChatTag(intent, portal, user.bridge.Config.Bridge.PinnedTag, chat.Pinned)
}
}
func (user *User) getDirectChats() map[id.UserID][]id.RoomID {
res := make(map[id.UserID][]id.RoomID)
- privateChats := user.bridge.DB.Portal.FindPrivateChats(user.JID)
+ privateChats := user.bridge.DB.Portal.FindPrivateChats(user.JID.ToNonAD())
for _, portal := range privateChats {
if len(portal.MXID) > 0 {
res[user.bridge.FormatPuppetMXID(portal.Key.JID)] = []id.RoomID{portal.MXID}
@@ -932,372 +625,90 @@ func (user *User) UpdateDirectChats(chats map[id.UserID][]id.RoomID) {
}
}
-func (user *User) HandleContactList(contacts []whatsapp.Contact) {
- contactMap := make(map[whatsapp.JID]whatsapp.Contact)
- for _, contact := range contacts {
- contactMap[contact.JID] = contact
- }
- go user.syncPuppets(contactMap)
-}
-
-func (user *User) syncPuppets(contacts map[whatsapp.JID]whatsapp.Contact) {
- if contacts == nil {
- contacts = user.Conn.Store.Contacts
- }
-
- _, hasSelf := contacts[user.JID]
- if !hasSelf {
- contacts[user.JID] = whatsapp.Contact{
- Name: user.pushName,
- Notify: user.pushName,
- JID: user.JID,
- }
- }
-
- user.log.Infoln("Syncing puppet info from contacts")
- for jid, contact := range contacts {
- if strings.HasSuffix(jid, whatsapp.NewUserSuffix) {
- puppet := user.bridge.GetPuppetByJID(contact.JID)
- puppet.Sync(user, contact)
- } else if strings.HasSuffix(jid, whatsapp.BroadcastSuffix) {
- portal := user.GetPortalByJID(contact.JID)
- portal.Sync(user, contact)
- }
- }
- user.log.Infoln("Finished syncing puppet info from contacts")
-}
-
-func (user *User) updateLastConnectionIfNecessary() {
- if user.LastConnection+60 < time.Now().Unix() {
- user.UpdateLastConnection()
- }
-}
-
-func (user *User) HandleError(err error) {
- if !errors.Is(err, whatsapp.ErrInvalidWsData) {
- user.log.Errorfln("WhatsApp error: %v", err)
- }
- if closed, ok := err.(*whatsapp.ErrConnectionClosed); ok {
- if user.Session == nil {
- user.log.Debugln("Websocket disconnected, but no session stored, not trying to reconnect")
- return
- }
- user.bridge.Metrics.TrackDisconnection(user.MXID)
- if closed.Code == 1000 && user.cleanDisconnection {
- user.cleanDisconnection = false
- if !user.bridge.Config.Bridge.AggressiveReconnect {
- user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
- user.bridge.Metrics.TrackConnectionState(user.JID, false)
- user.log.Infoln("Clean disconnection by server")
- return
- } else {
- user.log.Debugln("Clean disconnection by server, but aggressive reconnection is enabled")
- }
- }
- go user.tryReconnect(fmt.Sprintf("Your WhatsApp connection was closed with websocket status code %d", closed.Code))
- } else if failed, ok := err.(*whatsapp.ErrConnectionFailed); ok {
- disconnectErr := user.Conn.Disconnect()
- if disconnectErr != nil {
- user.log.Warnln("Failed to disconnect after connection fail:", disconnectErr)
- }
- user.bridge.Metrics.TrackDisconnection(user.MXID)
- user.ConnectionErrors++
- go user.tryReconnect(fmt.Sprintf("Your WhatsApp connection failed: %v", failed.Err))
- } else if err == whatsapp.ErrPingFalse || err == whatsapp.ErrWebsocketKeepaliveFailed {
- disconnectErr := user.Conn.Disconnect()
- if disconnectErr != nil {
- user.log.Warnln("Failed to disconnect after failed ping:", disconnectErr)
- }
- user.bridge.Metrics.TrackDisconnection(user.MXID)
- user.ConnectionErrors++
- go user.tryReconnect(fmt.Sprintf("Your WhatsApp connection failed: %v", err))
- }
- // Otherwise unknown error, probably mostly harmless
-}
-
-var reconnectionID int64
-
-func (user *User) tryReconnect(msg string) {
- localReconID := atomic.AddInt64(&reconnectionID, 1)
- rcLog := user.log.Sub(fmt.Sprintf("Reconnector-%d", localReconID))
- user.bridge.Metrics.TrackConnectionState(user.JID, false)
- if user.ConnectionErrors > user.bridge.Config.Bridge.MaxConnectionAttempts {
- user.sendMarkdownBridgeAlert("%s. Use the `reconnect` command to reconnect.", msg)
- user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
- return
- }
- if user.bridge.Config.Bridge.ReportConnectionRetry {
- user.sendBridgeNotice("%s. Reconnecting...", msg)
- // Don't want the same error to be repeated
- msg = ""
- }
- var tries uint
- var exponentialBackoff bool
- baseDelay := time.Duration(user.bridge.Config.Bridge.ConnectionRetryDelay)
- if baseDelay < 0 {
- exponentialBackoff = true
- baseDelay = -baseDelay + 1
- }
- delay := baseDelay
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- user.cancelReconnect = cancel
- for attemptNo := 1; user.ConnectionErrors <= user.bridge.Config.Bridge.MaxConnectionAttempts; attemptNo++ {
- select {
- case <-ctx.Done():
- rcLog.Debugln("tryReconnect context cancelled, aborting reconnection attempts")
- return
- default:
- }
- user.sendBridgeState(BridgeState{StateEvent: StateConnecting, Error: WAConnecting})
- rcLog.Debugfln("Starting reconnection attempt #%d", attemptNo)
- err := user.Conn.Restore(true, ctx)
- if err == nil {
- user.ConnectionErrors = 0
- if user.bridge.Config.Bridge.ReportConnectionRetry {
- user.sendBridgeNotice("Reconnected successfully")
- }
- rcLog.Debugln("Reconnection successful, running PostLogin and exiting loop")
- user.PostLogin()
- return
- } else if errors.Is(err, whatsapp.ErrBadRequest) {
- rcLog.Warnln("Got init 400 error when trying to reconnect, resetting connection...")
- err = user.Conn.Disconnect()
- if err != nil {
- rcLog.Debugln("Error while disconnecting for connection reset:", err)
- }
- } else if errors.Is(err, whatsapp.ErrUnpaired) || errors.Is(err, whatsapp.ErrInvalidSession) {
- rcLog.Errorfln("Got init %s error when trying to reconnect, not retrying", err)
- user.removeFromJIDMap(StateBadCredentials)
- //user.JID = ""
- user.SetSession(nil)
- user.DeleteConnection()
- errMsg := "unpaired from phone"
- if errors.Is(err, whatsapp.ErrInvalidSession) {
- errMsg = "invalid session"
- }
- user.sendMarkdownBridgeAlert("\u26a0 Failed to reconnect to WhatsApp: %s. " +
- "To re-pair your phone, log in again.", errMsg)
- return
- } else if errors.Is(err, whatsapp.ErrAlreadyLoggedIn) {
- rcLog.Warnln("Reconnection said we're already logged in, not trying anymore")
- return
- } else if errors.Is(err, whatsapp.ErrLoginInProgress) {
- rcLog.Warnln("Reconnection said another reconnection is in progress, hoping there's another thread trying to reconnect and not retrying here")
- return
- } else {
- rcLog.Errorln("Error while trying to reconnect after disconnection:", err)
- }
- tries++
- user.ConnectionErrors++
- if user.ConnectionErrors <= user.bridge.Config.Bridge.MaxConnectionAttempts {
- if exponentialBackoff {
- delay = (1 << tries) + baseDelay
- }
- if user.bridge.Config.Bridge.ReportConnectionRetry {
- user.sendBridgeNotice("Reconnection attempt failed: %v. Retrying in %d seconds...", err, delay)
- }
- time.Sleep(delay * time.Second)
- }
- }
- rcLog.Debugln("Giving up trying to reconnect")
-
- user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
- if user.bridge.Config.Bridge.ReportConnectionRetry {
- user.sendMarkdownBridgeAlert("%d reconnection attempts failed. Use the `reconnect` command to try to reconnect manually.", tries)
+func (user *User) handleLoggedOut(onConnect bool) {
+ user.JID = types.EmptyJID
+ user.Update()
+ if onConnect {
+ user.sendMarkdownBridgeAlert("Connecting to WhatsApp failed as the device was logged out. Please link the bridge to your phone again.")
} else {
- user.sendMarkdownBridgeAlert("\u26a0 %s. Additionally, %d reconnection attempts failed. Use the `reconnect` command to try to reconnect.", msg, tries)
+ user.sendMarkdownBridgeAlert("You were logged out from another device. Please link the bridge to your phone again.")
}
+ user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotLoggedIn})
}
-func (user *User) PortalKey(jid whatsapp.JID) database.PortalKey {
- return database.NewPortalKey(jid, user.JID)
+func (user *User) GetPortalByJID(jid types.JID) *Portal {
+ return user.bridge.GetPortalByJID(database.NewPortalKey(jid, user.JID))
}
-func (user *User) GetPortalByJID(jid whatsapp.JID) *Portal {
- return user.bridge.GetPortalByJID(user.PortalKey(jid))
+func (user *User) syncPuppet(jid types.JID) {
+ user.bridge.GetPuppetByJID(jid).SyncContact(user, false)
}
-func (user *User) runMessageRingBuffer() {
- for msg := range user.messageInput {
- select {
- case user.messageOutput <- msg:
- user.bridge.Metrics.TrackBufferLength(user.MXID, len(user.messageOutput))
- default:
- dropped := <-user.messageOutput
- user.log.Warnln("Buffer is full, dropping message in", dropped.chat)
- user.messageOutput <- msg
- }
- }
-}
+const WATypingTimeout = 15 * time.Second
-func (user *User) handleMessageLoop() {
- for {
- select {
- case msg := <-user.messageOutput:
- user.bridge.Metrics.TrackBufferLength(user.MXID, len(user.messageOutput))
- user.GetPortalByJID(msg.chat).messages <- msg
- case <-user.syncStart:
- user.log.Debugln("Processing of incoming messages is locked")
- user.bridge.Metrics.TrackSyncLock(user.JID, true)
- user.syncWait.Wait()
- user.bridge.Metrics.TrackSyncLock(user.JID, false)
- user.log.Debugln("Processing of incoming messages unlocked")
- }
- }
-}
-
-func (user *User) HandleNewContact(contact whatsapp.Contact) {
- user.log.Debugfln("Contact message: %+v", contact)
- if strings.HasSuffix(contact.JID, whatsapp.OldUserSuffix) {
- contact.JID = strings.Replace(contact.JID, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, -1)
- }
- if strings.HasSuffix(contact.JID, whatsapp.NewUserSuffix) {
- puppet := user.bridge.GetPuppetByJID(contact.JID)
- puppet.UpdateName(user, contact)
- } else if strings.HasSuffix(contact.JID, whatsapp.BroadcastSuffix) {
- portal := user.GetPortalByJID(contact.JID)
- portal.UpdateName(contact.Name, "", nil, true)
- }
-}
-
-func (user *User) HandleBatteryMessage(battery whatsapp.BatteryMessage) {
- user.log.Debugfln("Battery message: %+v", battery)
- var notice string
- if !battery.Plugged && battery.Percentage < 15 && user.batteryWarningsSent < 1 {
- notice = fmt.Sprintf("Phone battery low (%d %% remaining)", battery.Percentage)
- user.batteryWarningsSent = 1
- } else if !battery.Plugged && battery.Percentage < 5 && user.batteryWarningsSent < 2 {
- notice = fmt.Sprintf("Phone battery very low (%d %% remaining)", battery.Percentage)
- user.batteryWarningsSent = 2
- } else if battery.Percentage > 15 || battery.Plugged {
- user.batteryWarningsSent = 0
- }
- if notice != "" {
- go user.sendBridgeNotice("%s", notice)
- }
-}
-
-type FakeMessage struct {
- Text string
- ID string
- Alert bool
-}
-
-func (user *User) HandleCallInfo(info whatsapp.CallInfo) {
- if info.Data != nil {
+func (user *User) handleChatPresence(presence *events.ChatPresence) {
+ puppet := user.bridge.GetPuppetByJID(presence.Sender)
+ portal := user.GetPortalByJID(presence.Chat)
+ if puppet == nil || portal == nil || len(portal.MXID) == 0 {
return
}
- data := FakeMessage{
- ID: info.ID,
- }
- switch info.Type {
- case whatsapp.CallOffer:
- if !user.bridge.Config.Bridge.CallNotices.Start {
- return
- }
- data.Text = "Incoming call"
- data.Alert = true
- case whatsapp.CallOfferVideo:
- if !user.bridge.Config.Bridge.CallNotices.Start {
- return
- }
- data.Text = "Incoming video call"
- data.Alert = true
- case whatsapp.CallTerminate:
- if !user.bridge.Config.Bridge.CallNotices.End {
- return
- }
- data.Text = "Call ended"
- data.ID += "E"
- default:
- return
- }
- portal := user.GetPortalByJID(info.From)
- if portal != nil {
- portal.messages <- PortalMessage{info.From, user, data, 0}
- }
-}
-
-func (user *User) HandlePresence(info whatsapp.PresenceEvent) {
- puppet := user.bridge.GetPuppetByJID(info.SenderJID)
- switch info.Status {
- case whatsapp.PresenceUnavailable:
- _ = puppet.DefaultIntent().SetPresence("offline")
- case whatsapp.PresenceAvailable:
- if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() {
- portal := user.bridge.GetPortalByMXID(puppet.typingIn)
- _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0)
- puppet.typingIn = ""
- puppet.typingAt = 0
- } else {
- _ = puppet.DefaultIntent().SetPresence("online")
- }
- case whatsapp.PresenceComposing:
- portal := user.GetPortalByJID(info.JID)
- if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() {
+ if presence.State == types.ChatPresenceComposing {
+ if puppet.typingIn != "" && puppet.typingAt.Add(WATypingTimeout).Before(time.Now()) {
if puppet.typingIn == portal.MXID {
return
}
_, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0)
}
- if len(portal.MXID) > 0 {
- puppet.typingIn = portal.MXID
- puppet.typingAt = time.Now().Unix()
- _, _ = puppet.IntentFor(portal).UserTyping(portal.MXID, true, 15*1000)
- }
- }
-}
-
-func (user *User) HandleMsgInfo(info whatsapp.JSONMsgInfo) {
- if (info.Command == whatsapp.MsgInfoCommandAck || info.Command == whatsapp.MsgInfoCommandAcks) && info.Acknowledgement == whatsapp.AckMessageRead {
- portal := user.GetPortalByJID(info.ToJID)
- if len(portal.MXID) == 0 {
- return
- }
-
- intent := user.bridge.GetPuppetByJID(info.SenderJID).IntentFor(portal)
- for _, msgID := range info.IDs {
- msg := user.bridge.DB.Message.GetByJID(portal.Key, msgID)
- if msg == nil || msg.IsFakeMXID() {
- continue
- }
-
- err := intent.MarkReadWithContent(portal.MXID, msg.MXID, &CustomReadReceipt{DoublePuppet: intent.IsCustomPuppet})
- if err != nil {
- user.log.Warnfln("Failed to mark message %s as read by %s: %v", msg.MXID, info.SenderJID, err)
- }
- }
- }
-}
-
-func (user *User) HandleReceivedMessage(received whatsapp.ReceivedMessage) {
- if received.Type == "read" {
- go user.markSelfRead(received.Jid, received.Index)
+ _, _ = puppet.IntentFor(portal).UserTyping(portal.MXID, true, WATypingTimeout.Milliseconds())
+ puppet.typingIn = portal.MXID
+ puppet.typingAt = time.Now()
} else {
- user.log.Debugfln("Unknown received message type: %+v", received)
+ _, _ = puppet.IntentFor(portal).UserTyping(portal.MXID, false, 0)
+ puppet.typingIn = ""
}
}
-func (user *User) HandleReadMessage(read whatsapp.ReadMessage) {
- user.log.Debugfln("Received chat read message: %+v", read)
- go user.markSelfRead(read.Jid, "")
+func (user *User) handleReceipt(receipt *events.Receipt) {
+ if receipt.Type != events.ReceiptTypeRead {
+ return
+ }
+ portal := user.GetPortalByJID(receipt.Chat)
+ if portal == nil || len(portal.MXID) == 0 {
+ return
+ }
+ if receipt.IsFromMe {
+ user.markSelfRead(portal, receipt.MessageID)
+ } else {
+ intent := user.bridge.GetPuppetByJID(receipt.Sender).IntentFor(portal)
+ ok := user.markOtherRead(portal, intent, receipt.MessageID)
+ if !ok {
+ // Message not found, try any previous IDs
+ for i := len(receipt.PreviousIDs) - 1; i >= 0; i-- {
+ ok = user.markOtherRead(portal, intent, receipt.PreviousIDs[i])
+ if ok {
+ break
+ }
+ }
+ }
+ }
}
-func (user *User) markSelfRead(jid, messageID string) {
- if strings.HasSuffix(jid, whatsapp.OldUserSuffix) {
- jid = strings.Replace(jid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, -1)
+func (user *User) markOtherRead(portal *Portal, intent *appservice.IntentAPI, messageID types.MessageID) bool {
+ msg := user.bridge.DB.Message.GetByJID(portal.Key, messageID)
+ if msg == nil || msg.IsFakeMXID() {
+ return false
}
- puppet := user.bridge.GetPuppetByJID(user.JID)
- if puppet == nil {
- return
+
+ err := intent.MarkReadWithContent(portal.MXID, msg.MXID, &CustomReadReceipt{DoublePuppet: intent.IsCustomPuppet})
+ if err != nil {
+ user.log.Warnfln("Failed to mark message %s as read by %s: %v", msg.MXID, intent.UserID, err)
}
- intent := puppet.CustomIntent()
- if intent == nil {
- return
- }
- portal := user.GetPortalByJID(jid)
- if portal == nil {
+ return true
+}
+
+func (user *User) markSelfRead(portal *Portal, messageID types.MessageID) {
+ puppet := user.bridge.GetPuppetByCustomMXID(user.MXID)
+ if puppet == nil || puppet.CustomIntent() == nil {
return
}
var message *database.Message
@@ -1314,120 +725,66 @@ func (user *User) markSelfRead(jid, messageID string) {
}
user.log.Debugfln("User read message %s/%s in %s/%s in WhatsApp mobile", message.JID, message.MXID, portal.Key.JID, portal.MXID)
}
- err := intent.MarkReadWithContent(portal.MXID, message.MXID, &CustomReadReceipt{DoublePuppet: true})
+ err := puppet.CustomIntent().MarkReadWithContent(portal.MXID, message.MXID, &CustomReadReceipt{DoublePuppet: true})
if err != nil {
- user.log.Warnfln("Failed to bridge own read receipt in %s: %v", jid, err)
+ user.log.Warnfln("Failed to bridge own read receipt in %s: %v", portal.Key.JID, err)
}
}
-func (user *User) HandleCommand(cmd whatsapp.JSONCommand) {
- switch cmd.Type {
- case whatsapp.CommandPicture:
- if strings.HasSuffix(cmd.JID, whatsapp.NewUserSuffix) {
- puppet := user.bridge.GetPuppetByJID(cmd.JID)
- go puppet.UpdateAvatar(user, cmd.ProfilePicInfo)
- } else if user.bridge.Config.Bridge.ChatMetaSync {
- portal := user.GetPortalByJID(cmd.JID)
- go portal.UpdateAvatar(user, cmd.ProfilePicInfo, true)
- }
- case whatsapp.CommandDisconnect:
- if cmd.Kind == "replaced" {
- user.cleanDisconnection = true
- go user.sendMarkdownBridgeAlert("\u26a0 Your WhatsApp connection was closed by the server because you opened another WhatsApp Web client.\n\n" +
- "Use the `reconnect` command to disconnect the other client and resume bridging.")
- } else {
- user.log.Warnln("Unknown kind of disconnect:", string(cmd.Raw))
- go user.sendMarkdownBridgeAlert("\u26a0 Your WhatsApp connection was closed by the server (reason code: %s).\n\n"+
- "Use the `reconnect` command to reconnect.", cmd.Kind)
- }
- }
-}
-
-func (user *User) HandleChatUpdate(cmd whatsapp.ChatUpdate) {
- if cmd.Command != whatsapp.ChatUpdateCommandAction {
+func (user *User) markSelfReadFull(portal *Portal) {
+ puppet := user.bridge.GetPuppetByCustomMXID(user.MXID)
+ if puppet == nil || puppet.CustomIntent() == nil {
return
}
-
- portal := user.GetPortalByJID(cmd.JID)
- if len(portal.MXID) == 0 {
- if cmd.Data.Action == whatsapp.ChatActionIntroduce || cmd.Data.Action == whatsapp.ChatActionCreate {
- go func() {
- err := portal.CreateMatrixRoom(user)
- if err != nil {
- user.log.Errorln("Failed to create portal room after receiving join event:", err)
- }
- }()
- }
+ lastMessage := user.bridge.DB.Message.GetLastInChat(portal.Key)
+ if lastMessage == nil {
return
}
-
- // These don't come down the message history :(
- switch cmd.Data.Action {
- case whatsapp.ChatActionAddTopic:
- go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID, nil, true)
- case whatsapp.ChatActionRemoveTopic:
- go portal.UpdateTopic("", cmd.Data.SenderJID, nil, true)
- case whatsapp.ChatActionRemove:
- // We ignore leaving groups in the message history to avoid accidentally leaving rejoined groups,
- // but if we get a real-time command that says we left, it should be safe to bridge it.
- if !user.bridge.Config.Bridge.ChatMetaSync {
- for _, jid := range cmd.Data.UserChange.JIDs {
- if jid == user.JID {
- go portal.HandleWhatsAppKick(nil, cmd.Data.SenderJID, cmd.Data.UserChange.JIDs)
- break
- }
- }
- }
+ err := puppet.CustomIntent().MarkReadWithContent(portal.MXID, lastMessage.MXID, &CustomReadReceipt{DoublePuppet: true})
+ if err != nil {
+ user.log.Warnfln("Failed to mark %s in %s as read after backfill: %v", lastMessage.MXID, portal.MXID, err)
}
+}
- if !user.bridge.Config.Bridge.ChatMetaSync {
- // Ignore chat update commands, we're relying on the message history.
+func (user *User) handleGroupUpdate(evt *events.GroupInfo) {
+ portal := user.GetPortalByJID(evt.JID)
+ if portal == nil || len(portal.MXID) == 0 {
+ // TODO create portal when added to group
+ user.log.Debugfln("Ignoring group info update in chat with no portal: %+v", evt)
return
}
+ switch {
+ case evt.Announce != nil:
+ portal.RestrictMessageSending(evt.Announce.IsAnnounce)
+ case evt.Locked != nil:
+ portal.RestrictMetadataChanges(evt.Locked.IsLocked)
+ case evt.Name != nil:
+ portal.UpdateName(evt.Name.Name, evt.Name.NameSetBy, true)
+ case evt.Topic != nil:
+ portal.UpdateTopic(evt.Topic.Topic, evt.Topic.TopicSetBy, true)
+ case evt.Leave != nil:
+ if evt.Sender != nil && !evt.Sender.IsEmpty() {
+ portal.HandleWhatsAppKick(user, *evt.Sender, evt.Leave)
+ }
+ case evt.Join != nil:
+ portal.HandleWhatsAppInvite(user, evt.Sender, evt.Join)
+ case evt.Promote != nil:
+ portal.ChangeAdminStatus(evt.Promote, true)
+ case evt.Demote != nil:
+ portal.ChangeAdminStatus(evt.Demote, false)
+ }
+}
- switch cmd.Data.Action {
- case whatsapp.ChatActionNameChange:
- go portal.UpdateName(cmd.Data.NameChange.Name, cmd.Data.SenderJID, nil, true)
- case whatsapp.ChatActionPromote:
- go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, true)
- case whatsapp.ChatActionDemote:
- go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, false)
- case whatsapp.ChatActionAnnounce:
- go portal.RestrictMessageSending(cmd.Data.Announce)
- case whatsapp.ChatActionRestrict:
- go portal.RestrictMetadataChanges(cmd.Data.Restrict)
- case whatsapp.ChatActionRemove:
- go portal.HandleWhatsAppKick(nil, cmd.Data.SenderJID, cmd.Data.UserChange.JIDs)
- case whatsapp.ChatActionAdd:
- go portal.HandleWhatsAppInvite(user, cmd.Data.SenderJID, nil, cmd.Data.UserChange.JIDs)
- case whatsapp.ChatActionIntroduce:
- if cmd.Data.SenderJID != "unknown" {
- go portal.Sync(user, whatsapp.Contact{JID: portal.Key.JID})
+func (user *User) handlePictureUpdate(evt *events.Picture) {
+ if evt.JID.Server == types.DefaultUserServer {
+ puppet := user.bridge.GetPuppetByJID(evt.JID)
+ if puppet.Avatar != evt.PictureID {
+ puppet.UpdateAvatar(user)
+ }
+ } else {
+ portal := user.GetPortalByJID(evt.JID)
+ if portal != nil && portal.Avatar != evt.PictureID {
+ portal.UpdateAvatar(user, evt.Author, true)
}
}
}
-
-func (user *User) HandleConnInfo(info whatsapp.ConnInfo) {
- if user.Session != nil && info.Connected && len(info.ClientToken) > 0 {
- user.log.Debugln("Received new tokens")
- user.Session.ClientToken = info.ClientToken
- user.Session.ServerToken = info.ServerToken
- user.Session.Wid = info.WID
- user.Update()
- }
- if len(info.PushName) > 0 {
- user.pushName = info.PushName
- }
-}
-
-func (user *User) HandleJSONMessage(evt whatsapp.RawJSONMessage) {
- if !json.Valid(evt.RawMessage) {
- return
- }
- user.log.Debugfln("JSON message with tag %s: %s", evt.Tag, evt.RawMessage)
- user.updateLastConnectionIfNecessary()
-}
-
-func (user *User) NeedsRelaybot(portal *Portal) bool {
- return !user.HasSession() || !user.IsInPortal(portal.Key)
-}