Watch
1
0
Fork
You've already forked mautrix-whatsapp
0
mirror of https://github.com/mautrix/whatsapp.git synced 2026-11-10 08:09:19 -05:00

Compare commits

..
Author SHA1 Message Date
Nick Mills-Barrett
8d497bb9fb
capabilities: allow avatar and topic when creating groups
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:49:31 +01:00
39 changed files with 217 additions and 991 deletions

View file

@ -1 +0,0 @@
code_format_allowlist: ["wa-connect-failure-%d"]

View file

@ -30,7 +30,6 @@ jobs:
run: | run: |
go install golang.org/x/tools/cmd/goimports@latest go install golang.org/x/tools/cmd/goimports@latest
go install honnef.co/go/tools/cmd/staticcheck@latest go install honnef.co/go/tools/cmd/staticcheck@latest
go install github.com/beeper/pre-commit-go/bridgelint@latest
export PATH="$HOME/go/bin:$PATH" export PATH="$HOME/go/bin:$PATH"
- name: Run pre-commit - name: Run pre-commit

View file

@ -21,12 +21,9 @@ repos:
- id: go-mod-tidy - id: go-mod-tidy
- repo: https://github.com/beeper/pre-commit-go - repo: https://github.com/beeper/pre-commit-go
rev: v0.5.0 rev: v0.4.2
hooks: hooks:
- id: zerolog-ban-msgf - id: zerolog-ban-msgf
- id: zerolog-use-stringer - id: zerolog-use-stringer
- id: prevent-literal-http-methods - id: prevent-literal-http-methods
- id: zerolog-ban-global-log - id: zerolog-ban-global-log
- id: bridgelint
language: system
verbose: true

View file

@ -1,28 +1,3 @@
# v26.10
* Added support for new rich response messages used by the Muse AI bot.
* Added support for bridging custom profile set for Muse.
* Added automatic one-time resync of keys needed to decrypt Muse messages.
* Added support for blocking and unblocking users on WhatsApp.
* Added support for contact array messages (thanks to [@voska] in [#961]).
* Added partial support for filling quoted message when sending replies to
WhatsApp.
* Changed undecryptable message resends to only bridge as edits and never as new
messages, to avoid races if the message was deleted before the resend.
* Changed login to wait for OTKs to be successfully uploaded, as the login will
fail if the bridge is stopped before that point.
* Changed chat info syncing to not bridge mute status if app state sync is
failing.
* Changed power levels to allow poll votes in read-only groups even if the room
is encrypted.
* Fixed backfilled media retries not getting sent if configured for local time.
* Fixed "delete for me" from WhatsApp not being bridged correctly in DMs.
* Fixed very long WhatsApp messages not being bridged.
* Fixed document message caption edits not being handled correctly.
[@voska]: https://github.com/voska
[#961]: https://github.com/mautrix/whatsapp/pull/961
# v26.09 # v26.09
* Bumped minimum Go version to 1.26. * Bumped minimum Go version to 1.26.

View file

@ -18,7 +18,7 @@ var m = mxmain.BridgeMain{
Name: "mautrix-whatsapp", Name: "mautrix-whatsapp",
URL: "https://github.com/mautrix/whatsapp", URL: "https://github.com/mautrix/whatsapp",
Description: "A Matrix-WhatsApp puppeting bridge.", Description: "A Matrix-WhatsApp puppeting bridge.",
Version: "26.10", Version: "26.09",
SemCalVer: true, SemCalVer: true,
Connector: &connector.WhatsAppConnector{}, Connector: &connector.WhatsAppConnector{},
} }

28
go.mod
View file

@ -2,7 +2,7 @@ module go.mau.fi/mautrix-whatsapp
go 1.26.0 go 1.26.0
toolchain go1.27.2 toolchain go1.27.1
tool go.mau.fi/util/cmd/maubuild tool go.mau.fi/util/cmd/maubuild
@ -10,17 +10,17 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/lib/pq v1.12.3 github.com/lib/pq v1.12.3
github.com/rs/zerolog v1.35.1 github.com/rs/zerolog v1.35.1
github.com/tidwall/gjson v1.20.0 github.com/tidwall/gjson v1.19.0
github.com/yuin/goldmark v1.8.6 github.com/yuin/goldmark v1.8.6
go.mau.fi/util v0.11.0 go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8
go.mau.fi/webp v0.3.0 go.mau.fi/webp v0.3.0
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a go.mau.fi/whatsmeow v0.0.0-20261013142727-4cf8bc8e0aeb
golang.org/x/image v0.47.0 golang.org/x/image v0.46.0
golang.org/x/net v0.61.0 golang.org/x/net v0.59.0
golang.org/x/sync v0.24.0 golang.org/x/sync v0.23.0
google.golang.org/protobuf v1.36.12 google.golang.org/protobuf v1.36.12
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445 maunium.net/go/mautrix v0.31.1-0.20261005174338-57e1ea5dd74d
) )
require ( require (
@ -33,7 +33,7 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.52 // indirect github.com/mattn/go-sqlite3 v1.14.52 // indirect
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b // indirect github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect github.com/rs/xid v1.6.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
@ -43,11 +43,11 @@ require (
github.com/vektah/gqlparser/v2 v2.5.27 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect
go.mau.fi/libsignal v0.2.2 // indirect go.mau.fi/libsignal v0.2.2 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/crypto v0.58.0 // indirect golang.org/x/crypto v0.57.0 // indirect
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e // indirect golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect
golang.org/x/mod v0.42.0 // indirect golang.org/x/mod v0.41.0 // indirect
golang.org/x/sys v0.49.0 // indirect golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.43.0 // indirect golang.org/x/text v0.42.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect maunium.net/go/mauflag v1.0.0 // indirect

52
go.sum
View file

@ -34,8 +34,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U= github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b h1:OzNsuVdSWGwXvWKTtChx9ve89k4cFJ6NxYM4Aw4/f+E= github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 h1:lcWAnrqr2nNfDiArwFNHCE4787Mw2tCdVSOXCru0/0E=
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@ -51,8 +51,8 @@ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDq
github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto= github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.20.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
@ -67,31 +67,31 @@ github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU= go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU=
go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0= go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0=
go.mau.fi/util v0.11.0 h1:JNEpkVRxfTdJUC0DIGKiRlLKzl4G/qLX/eVzoP7QuwY= go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcUkqKshE+n239UtbbJOn4=
go.mau.fi/util v0.11.0/go.mod h1:04AEFVsc4zNdIkDC1gkdqbg/nZFolHY50pyXh2DSy7s= go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc=
go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA=
go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo=
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a h1:ZyCV/mWfRkutxs5R0K3v+oyTbF4huprSlPZg/LEFQ7k= go.mau.fi/whatsmeow v0.0.0-20261013142727-4cf8bc8e0aeb h1:yRvCcaQwqHMpUrV9Cn48QOw/YSsV1BlXCs00BTRz0Ks=
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a/go.mod h1:iY4lm61nTLWsNiIE1EmHUZwhKGBeoV4dhnVE2uMtNoQ= go.mau.fi/whatsmeow v0.0.0-20261013142727-4cf8bc8e0aeb/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e h1:HgI6W1OolKK6tcV74is+rl9SMT0njsHlp0TbQiGXI/A= golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU=
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e/go.mod h1:DBoxwWhjaW8DJJw/YeM4zpeOKMC1FMXSblsYy+1mFSw= golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E=
golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo= golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
golang.org/x/image v0.47.0/go.mod h1:IkN5o4A7vq55vXN8FWjjnhg9Rs2GOtffqsSi9mWAKYE= golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= 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/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445 h1:ZI9Y2YEmjqQDloOCiciHeynTRYjUwK0ZZbNcRH/5NOc= maunium.net/go/mautrix v0.31.1-0.20261005174338-57e1ea5dd74d h1:sOdF5AF9MiU2mudfMBgQC79FJrhBBDbUkxQCi0T2uHI=
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= maunium.net/go/mautrix v0.31.1-0.20261005174338-57e1ea5dd74d/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk=

View file

@ -142,20 +142,11 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
Str("peer_data_request_session_id", evt.GetPeerDataRequestSessionID()). Str("peer_data_request_session_id", evt.GetPeerDataRequestSessionID()).
Msg("Downloading history sync") Msg("Downloading history sync")
blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true) blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true)
if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) { if err != nil {
log.Err(err).Msg("History sync blob is no longer available, dropping notification")
if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil {
log.Err(err).Msg("Failed to delete queued history sync notification")
errored = true
}
return
} else if err != nil {
log.Err(err).Msg("Failed to download history sync") log.Err(err).Msg("Failed to download history sync")
errored = true
return return
} }
if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND { if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND {
wa.finishOnDemandHistorySyncRequest(evt.GetPeerDataRequestSessionID())
wa.handleOnDemandHistorySync(ctx, blob) wa.handleOnDemandHistorySync(ctx, blob)
if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil { if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil {
log.Err(err).Msg("Failed to delete queued on-demand history sync notification") log.Err(err).Msg("Failed to delete queued on-demand history sync notification")
@ -179,7 +170,6 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
}) })
if err != nil { if err != nil {
log.Err(err).Msg("Failed to store history sync notification data") log.Err(err).Msg("Failed to store history sync notification data")
errored = true
} else { } else {
resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP || resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP ||
blob.GetSyncType() == waHistorySync.HistorySync_RECENT || blob.GetSyncType() == waHistorySync.HistorySync_RECENT ||
@ -255,17 +245,10 @@ func (wa *WhatsAppClient) handleWAHistorySync(
} else if jid.Server == types.BroadcastServer { } else if jid.Server == types.BroadcastServer {
log.Debug().Stringer("chat_jid", jid).Msg("Skipping broadcast list in history sync") log.Debug().Stringer("chat_jid", jid).Msg("Skipping broadcast list in history sync")
continue continue
} else if jid == types.PSAJID {
// New messages in the official updates chat aren't bridged because they use a different path,
// so drop history as well. This should be removed if the PSA chat gets properly bridged in the future.
log.Debug().Stringer("chat_jid", jid).Msg("Skipping PSA in history sync")
continue
} else { } else {
totalMessageCount += len(conv.GetMessages()) totalMessageCount += len(conv.GetMessages())
} }
if jid == types.MetaAIJID { if jid.Server == types.DefaultUserServer {
jid = types.NewMetaAIJID
} else if jid.Server == types.DefaultUserServer {
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid)
if err != nil { if err != nil {
log.Err(err).Stringer("pn", jid).Msg("Failed to get LID for phone number in history sync") log.Err(err).Stringer("pn", jid).Msg("Failed to get LID for phone number in history sync")
@ -298,9 +281,6 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Int("msg_index", i). Int("msg_index", i).
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()). Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()). Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()).
Stringer("stub_type", rawMsg.GetMessage().GetMessageStubType()).
Str("message_type", getMessageType(rawMsg.GetMessage().GetMessage())).
Bool("from_me", rawMsg.GetMessage().GetKey().GetFromMe()).
Msg("Dropping historical message due to parse error") Msg("Dropping historical message due to parse error")
log.Trace(). log.Trace().
Any("web_message_info", rawMsg.GetMessage()). Any("web_message_info", rawMsg.GetMessage()).
@ -454,7 +434,7 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
return return
} }
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true) wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true)
if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) || errors.Is(err, ErrNewsletterNotFound) { if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) {
log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID). log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID).
Msg("Skipping creating room because the user is not a participant") Msg("Skipping creating room because the user is not a participant")
//err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID) //err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID)
@ -605,36 +585,16 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
} }
} }
} }
resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true, !params.Forward && !hasMore) resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to convert messages: %w", err) return nil, fmt.Errorf("failed to convert messages: %w", err)
} }
// TODO might be better to always set true here and let GetBetween return an empty result to mark as false?
resp.HasMore = hasMore resp.HasMore = hasMore
resp.Forward = params.Forward resp.Forward = params.Forward
resp.MarkRead = markRead resp.MarkRead = markRead
if params.Forward && !markRead && conv != nil && !ptr.Val(conv.MarkedAsUnread) {
resp.ReadUpTo = historySyncReadUpTo(resp.Messages, int(ptr.Val(conv.UnreadCount)))
}
return resp, nil return resp, nil
} }
func historySyncReadUpTo(messages []*bridgev2.BackfillMessage, unreadCount int) time.Time {
if unreadCount <= 0 {
return time.Time{}
}
for i := len(messages) - 1; i >= 0; i-- {
if messages[i].Sender.IsFromMe {
continue
}
unreadCount--
if unreadCount == 0 {
return messages[i].Timestamp.Add(-time.Nanosecond)
}
}
return time.Time{}
}
func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) { func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) {
var err error var err error
var rows int64 var rows int64
@ -667,7 +627,6 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
portalJID types.JID, portalJID types.JID,
messages []*waWeb.WebMessageInfo, messages []*waWeb.WebMessageInfo,
explodeOnError bool, explodeOnError bool,
deleteEverything bool,
) (*bridgev2.FetchMessagesResponse, error) { ) (*bridgev2.FetchMessagesResponse, error) {
cli := wa.Client cli := wa.Client
if cli == nil { if cli == nil {
@ -704,9 +663,6 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, false) { if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, false) {
return nil, fmt.Errorf("failed to ensure alt JIDs for message %s", evt.Info.ID) return nil, fmt.Errorf("failed to ensure alt JIDs for message %s", evt.Info.ID)
} }
if evt.Info.IsFromMe {
wa.addPreviousOwnLID(ctx, evt.Info.Sender)
}
isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
converted, mediaReq := wa.convertHistorySyncMessage( converted, mediaReq := wa.convertHistorySyncMessage(
ctx, cli, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, ctx, cli, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
@ -726,11 +682,9 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
Messages: convertedMessages, Messages: convertedMessages,
Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)), Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)),
CompleteCallback: func() { CompleteCallback: func() {
if deleteEverything { // TODO this only deletes after backfilling. If there's no need for backfill after a relogin,
wa.deleteHistorySyncMessages(ctx, portalJID, 1<<62, oldestTS) // the messages will be stuck in the database
} else { wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS)
wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS)
}
if len(mediaRequests) > 0 { if len(mediaRequests) > 0 {
go func(ctx context.Context) { go func(ctx context.Context) {
for _, req := range mediaRequests { for _, req := range mediaRequests {
@ -752,28 +706,12 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID
if params.AnchorMessage == nil { if params.AnchorMessage == nil {
return nil, fmt.Errorf("anchor message is required to fetch messages from phone") return nil, fmt.Errorf("anchor message is required to fetch messages from phone")
} }
anchor := params.AnchorMessage parsed, err := waid.ParseMessageID(params.AnchorMessage.ID)
parsed, err := waid.ParseMessageID(anchor.ID)
if err != nil { if err != nil {
anchor, err = wa.Main.Bridge.DB.Message.GetFirstPortalMessageExcludingIDPattern(ctx, params.Portal.PortalKey, "fake:%") return nil, fmt.Errorf("failed to parse anchor message ID: %w", err)
if err != nil {
return nil, fmt.Errorf("failed to get first non-fake portal message: %w", err)
} else if anchor == nil {
return &bridgev2.FetchMessagesResponse{}, nil
}
parsed, err = waid.ParseMessageID(anchor.ID)
if err != nil {
return nil, fmt.Errorf("failed to parse anchor message ID: %w", err)
}
} }
msgID := wa.Client.GenerateMessageID() msgID := wa.Client.GenerateMessageID()
if !wa.startOnDemandHistorySyncRequest(ctx, portalJID, msgID) {
return &bridgev2.FetchMessagesResponse{
HasMore: true,
Pending: true,
}, nil
}
reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{ reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{
MessageSource: types.MessageSource{ MessageSource: types.MessageSource{
Chat: portalJID, Chat: portalJID,
@ -782,7 +720,7 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID
IsGroup: parsed.Chat.Server == types.GroupServer, IsGroup: parsed.Chat.Server == types.GroupServer,
}, },
ID: parsed.ID, ID: parsed.ID,
Timestamp: anchor.Timestamp, Timestamp: params.AnchorMessage.Timestamp,
}, 50) }, 50)
zerolog.Ctx(ctx).Debug(). zerolog.Ctx(ctx).Debug().
Str("request_msg_id", msgID). Str("request_msg_id", msgID).
@ -795,9 +733,6 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID
Peer: true, Peer: true,
}) })
if err != nil { if err != nil {
wa.onDemandHistorySyncLock.Lock()
delete(wa.onDemandHistorySyncRequests, portalJID)
wa.onDemandHistorySyncLock.Unlock()
return nil, fmt.Errorf("failed to send history sync request: %w", err) return nil, fmt.Errorf("failed to send history sync request: %w", err)
} }
return &bridgev2.FetchMessagesResponse{ return &bridgev2.FetchMessagesResponse{
@ -806,38 +741,6 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID
}, nil }, nil
} }
type onDemandHistorySyncRequest struct {
ID types.MessageID
SentAt time.Time
}
const onDemandHistorySyncRequestTimeout = 1 * time.Hour
func (wa *WhatsAppClient) startOnDemandHistorySyncRequest(ctx context.Context, portalJID types.JID, msgID types.MessageID) bool {
wa.onDemandHistorySyncLock.Lock()
defer wa.onDemandHistorySyncLock.Unlock()
if req, inFlight := wa.onDemandHistorySyncRequests[portalJID]; inFlight && time.Since(req.SentAt) < onDemandHistorySyncRequestTimeout {
zerolog.Ctx(ctx).Debug().
Str("in_flight_request_msg_id", req.ID).
Stringer("portal_jid", portalJID).
Time("sent_at", req.SentAt).
Msg("Not sending history sync request, previous request is still in flight")
return false
}
wa.onDemandHistorySyncRequests[portalJID] = onDemandHistorySyncRequest{ID: msgID, SentAt: time.Now()}
return true
}
func (wa *WhatsAppClient) finishOnDemandHistorySyncRequest(sessionID string) {
wa.onDemandHistorySyncLock.Lock()
defer wa.onDemandHistorySyncLock.Unlock()
for chatJID, req := range wa.onDemandHistorySyncRequests {
if req.ID == sessionID {
delete(wa.onDemandHistorySyncRequests, chatJID)
}
}
}
func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *waHistorySync.HistorySync) { func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *waHistorySync.HistorySync) {
if len(blob.GetConversations()) > 1 { if len(blob.GetConversations()) > 1 {
zerolog.Ctx(ctx).Warn(). zerolog.Ctx(ctx).Warn().
@ -850,9 +753,7 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w
zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID") zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID")
continue continue
} }
if portalJID == types.MetaAIJID { if portalJID.Server == types.DefaultUserServer {
portalJID = types.NewMetaAIJID
} else if portalJID.Server == types.DefaultUserServer {
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, portalJID) lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, portalJID)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("lid", portalJID).Msg("Failed to get LID for phone number in on-demand history sync") zerolog.Ctx(ctx).Err(err).Stringer("lid", portalJID).Msg("Failed to get LID for phone number in on-demand history sync")
@ -893,7 +794,7 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w
Int("message_count", len(messages)). Int("message_count", len(messages)).
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()). Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()).
Msg("Converting messages to bridge from on-demand history sync") Msg("Converting messages to bridge from on-demand history sync")
resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false, false) resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -30,6 +30,8 @@ var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
TypeDescription: "a group chat", TypeDescription: "a group chat",
Name: bridgev2.GroupFieldCapability{Allowed: true, MaxLength: 100}, Name: bridgev2.GroupFieldCapability{Allowed: true, MaxLength: 100},
Avatar: bridgev2.GroupFieldCapability{Allowed: true},
Topic: bridgev2.GroupFieldCapability{Allowed: true},
Disappear: bridgev2.GroupFieldCapability{Allowed: true, DisappearSettings: waDisappearingCap}, Disappear: bridgev2.GroupFieldCapability{Allowed: true, DisappearSettings: waDisappearingCap},
Participants: bridgev2.GroupFieldCapability{Allowed: true, Required: true, MinLength: 1}, Participants: bridgev2.GroupFieldCapability{Allowed: true, Required: true, MinLength: 1},
Parent: bridgev2.GroupFieldCapability{Allowed: true}, Parent: bridgev2.GroupFieldCapability{Allowed: true},

View file

@ -31,7 +31,6 @@ func (wa *WhatsAppClient) GetChatInfo(ctx context.Context, portal *bridgev2.Port
} }
var ErrBroadcastList = errors.New("broadcast list bridging is currently not supported") var ErrBroadcastList = errors.New("broadcast list bridging is currently not supported")
var ErrNewsletterNotFound = errors.New("newsletter doesn't exist")
func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation, isNew bool) (wrapped *bridgev2.ChatInfo, err error) { func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation, isNew bool) (wrapped *bridgev2.ChatInfo, err error) {
switch portalJID.Server { switch portalJID.Server {
@ -57,8 +56,6 @@ func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID,
info, err := wa.Client.GetNewsletterInfo(ctx, portalJID) info, err := wa.Client.GetNewsletterInfo(ctx, portalJID)
if err != nil { if err != nil {
return nil, err return nil, err
} else if info == nil || info.ID.IsEmpty() {
return nil, ErrNewsletterNotFound
} }
wrapped = wa.wrapNewsletterInfo(ctx, info) wrapped = wa.wrapNewsletterInfo(ctx, info)
default: default:

View file

@ -55,9 +55,6 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle), mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle),
pushNamesSynced: exsync.NewEvent(), pushNamesSynced: exsync.NewEvent(),
appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time), appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time),
previousOwnLIDs: exsync.NewSetWithItems(login.Metadata.(*waid.UserLoginMetadata).PreviousOwnLIDs),
onDemandHistorySyncRequests: make(map[types.JID]onDemandHistorySyncRequest),
disableNewsletter: store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS, disableNewsletter: store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS,
} }
@ -85,7 +82,6 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
w.Client.ManualHistorySyncDownload = true w.Client.ManualHistorySyncDownload = true
w.Client.SendReportingTokens = true w.Client.SendReportingTokens = true
w.Client.AutomaticMessageRerequestFromPhone = true w.Client.AutomaticMessageRerequestFromPhone = true
w.Client.UnifiedSessionInterval = 24 * time.Hour
w.Client.GetMessageForRetry = w.trackNotFoundRetry w.Client.GetMessageForRetry = w.trackNotFoundRetry
w.Client.PreRetryCallback = w.trackFoundRetry w.Client.PreRetryCallback = w.trackFoundRetry
w.Client.BackgroundEventCtx = w.UserLogin.Log.WithContext(wa.Bridge.BackgroundCtx) w.Client.BackgroundEventCtx = w.UserLogin.Log.WithContext(wa.Bridge.BackgroundCtx)
@ -136,12 +132,6 @@ type WhatsAppClient struct {
wasaResyncLock sync.Mutex wasaResyncLock sync.Mutex
museProfileLock sync.Mutex museProfileLock sync.Mutex
lastMuseProfileRequest time.Time lastMuseProfileRequest time.Time
onDemandHistorySyncLock sync.Mutex
onDemandHistorySyncRequests map[types.JID]onDemandHistorySyncRequest
previousOwnLIDs *exsync.Set[types.JID]
previousOwnLIDsLock sync.Mutex
} }
var ( var (
@ -210,26 +200,7 @@ func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID)
func (wa *WhatsAppClient) IsOwnJID(jid types.JID) bool { func (wa *WhatsAppClient) IsOwnJID(jid types.JID) bool {
return (jid.Server == types.DefaultUserServer && jid.User == wa.JID.User) || return (jid.Server == types.DefaultUserServer && jid.User == wa.JID.User) ||
(jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User) || (jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User)
wa.previousOwnLIDs.Has(jid.ToNonAD())
}
func (wa *WhatsAppClient) addPreviousOwnLID(ctx context.Context, jid types.JID) {
if jid.Server != types.HiddenUserServer || wa.IsOwnJID(jid) {
return
}
jid = jid.ToNonAD()
wa.previousOwnLIDsLock.Lock()
defer wa.previousOwnLIDsLock.Unlock()
if !wa.previousOwnLIDs.Add(jid) {
return
}
zerolog.Ctx(ctx).Info().Stringer("lid", jid).Msg("Found previous own LID in history sync")
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).PreviousOwnLIDs = wa.previousOwnLIDs.AsList()
err := wa.UserLogin.Save(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to save user login after adding previous own LID")
}
} }
func (wa *WhatsAppClient) GetLID() types.JID { func (wa *WhatsAppClient) GetLID() types.JID {

View file

@ -148,7 +148,13 @@ func fnSync(ce *commands.Event) {
case "appstate": case "appstate":
names := appstate.AllPatchNames[:] names := appstate.AllPatchNames[:]
if len(ce.Args) > 1 { if len(ce.Args) > 1 {
names = parseAppStateNames(ce, ce.Args[1:]) names = exslices.CastFuncFilter(ce.Args[1:], func(name string) (appstate.WAPatchName, bool) {
if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) {
ce.Reply("Invalid app state name `%s`", name)
return "", false
}
return appstate.WAPatchName(name), true
})
} }
for _, name := range names { for _, name := range names {
err := wa.Client.FetchAppState(ce.Ctx, name, true, false) err := wa.Client.FetchAppState(ce.Ctx, name, true, false)
@ -168,50 +174,6 @@ func fnSync(ce *commands.Event) {
} }
} }
func parseAppStateNames(ce *commands.Event, args []string) []appstate.WAPatchName {
return exslices.CastFuncFilter(args, func(name string) (appstate.WAPatchName, bool) {
if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) {
ce.Reply("Invalid app state name `%s`", name)
return "", false
}
return appstate.WAPatchName(name), true
})
}
var cmdResetAppState = &commands.FullHandler{
Func: fnResetAppState,
Name: "reset-appstate",
Help: commands.HelpMeta{
Section: commands.HelpSectionAdmin,
Description: "Ask your phone to reset broken app state. This logs out all linked devices.",
Args: "<names...>",
},
RequiresLogin: true,
}
func fnResetAppState(ce *commands.Event) {
login := ce.User.GetDefaultLogin()
if login == nil {
ce.Reply("Login not found")
return
}
if len(ce.Args) == 0 {
ce.Reply("Usage: `$cmdprefix reset-appstate <names...>`\n\nThis logs out all linked devices, including the bridge. You'll have to log in again afterwards.")
return
}
names := parseAppStateNames(ce, ce.Args)
if len(names) != len(ce.Args) {
return
}
wa := login.Client.(*WhatsAppClient)
_, err := wa.Client.SendPeerMessage(ce.Ctx, whatsmeow.BuildFatalAppStateExceptionNotification(names...))
if err != nil {
ce.Reply("Failed to send app state reset request: %v", err)
return
}
ce.Reply("Sent app state reset request. Your phone should log out all linked devices shortly, after which you can log in again.")
}
var cmdInviteLink = &commands.FullHandler{ var cmdInviteLink = &commands.FullHandler{
Func: fnInviteLink, Func: fnInviteLink,
Name: "invite-link", Name: "invite-link",

View file

@ -110,7 +110,7 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) {
wa.DB = wadb.New(bridge.ID, bridge.DB.Database, bridge.Log.With().Str("db_section", "whatsapp").Logger()) wa.DB = wadb.New(bridge.ID, bridge.DB.Database, bridge.Log.With().Str("db_section", "whatsapp").Logger())
wa.MsgConv.DB = wa.DB wa.MsgConv.DB = wa.DB
wa.Bridge.Commands.(*commands.Processor).AddHandlers( wa.Bridge.Commands.(*commands.Processor).AddHandlers(
cmdAccept, cmdSync, cmdResetAppState, cmdInviteLink, cmdResolveLink, cmdJoin, cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin,
) )
wa.mediaEditCache = make(MediaEditCache) wa.mediaEditCache = make(MediaEditCache)
wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]() wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]()
@ -164,19 +164,11 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to migrate to LID DMs: %w", err) return fmt.Errorf("failed to migrate to LID DMs: %w", err)
} }
err = wa.migrateMetaAIPortals(ctx)
if err != nil {
return fmt.Errorf("failed to migrate Meta AI portals: %w", err)
}
go func() { go func() {
err = wa.syncMismatchingGhosts(wa.Bridge.BackgroundCtx) err = wa.syncMismatchingGhosts(wa.Bridge.BackgroundCtx)
if err != nil { if err != nil {
wa.Bridge.Log.Err(err).Msg("Failed to sync mismatching ghosts") wa.Bridge.Log.Err(err).Msg("Failed to sync mismatching ghosts")
} }
err = wa.removeRejoinedPNGhosts(wa.Bridge.BackgroundCtx)
if err != nil {
wa.Bridge.Log.Err(err).Msg("Failed to remove rejoined PN ghosts")
}
}() }()
return nil return nil
} }
@ -243,16 +235,7 @@ func (wa *WhatsAppConnector) GenerateTransactionID(_ id.UserID, _ id.RoomID, _ e
} }
func (wa *WhatsAppConnector) ResetHTTPTransport() { func (wa *WhatsAppConnector) ResetHTTPTransport() {
for _, login := range wa.Bridge.GetAllCachedUserLogins() { // No-op for now, whatsmeow doesn't use the shared transport config yet
client := login.Client.(*WhatsAppClient).Client
if client == nil {
continue
}
err := wa.updateProxy(login.Log.WithContext(wa.Bridge.BackgroundCtx), client, false)
if err != nil {
login.Log.Err(err).Msg("Failed to recreate HTTP clients")
}
}
} }
func (wa *WhatsAppConnector) ResetNetworkConnections() { func (wa *WhatsAppConnector) ResetNetworkConnections() {

View file

@ -34,7 +34,6 @@ import (
"go.mau.fi/util/ptr" "go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waMmsRetry" "go.mau.fi/whatsmeow/proto/waMmsRetry"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events" "go.mau.fi/whatsmeow/types/events"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
@ -164,19 +163,7 @@ func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, par
return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin) return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin)
} }
sticker, err := wa.MsgConv.GetCachedSticker(ctx, waClient.Client, parsedID.Sticker.PackID, parsedID.Sticker.FileHash) sticker, err := wa.MsgConv.GetCachedSticker(ctx, waClient.Client, parsedID.Sticker.PackID, parsedID.Sticker.FileHash)
var httpErr whatsmeow.DownloadHTTPError if err != nil {
if errors.As(err, &httpErr) {
switch httpErr.StatusCode {
case http.StatusNotFound, http.StatusGone:
return nil, mautrix.MNotFound.WithMessage("Sticker pack not found")
case http.StatusUnauthorized, http.StatusForbidden:
return nil, mautrix.MForbidden.WithMessage("WhatsApp denied access to the sticker pack")
case http.StatusTooManyRequests:
return nil, mautrix.MLimitExceeded.WithMessage("Rate limited by WhatsApp while fetching the sticker pack").WithCanRetry(true)
default:
return nil, mautrix.MUnknown.WithMessage("WhatsApp returned HTTP %d while fetching the sticker pack", httpErr.StatusCode).WithStatus(http.StatusBadGateway).WithCanRetry(true)
}
} else if err != nil {
return nil, err return nil, err
} else if sticker == nil { } else if sticker == nil {
return nil, mautrix.MNotFound.WithMessage("Sticker not found in pack") return nil, mautrix.MNotFound.WithMessage("Sticker not found in pack")
@ -186,9 +173,6 @@ func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, par
func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) { func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String()) msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String())
if err == nil && msg == nil && parsedID.Message.Chat.Server == types.HiddenUserServer && parsedID.Message.Sender.Server == types.DefaultUserServer {
msg, err = wa.getDirectMediaMessageWithLIDSender(ctx, parsedID)
}
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get message: %w", err) return nil, fmt.Errorf("failed to get message: %w", err)
} else if msg == nil { } else if msg == nil {
@ -231,16 +215,6 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
return wa.makeDirectMediaResponse(ctx, waClient, keys, keys.MimeType, msg.ID, keys, params) return wa.makeDirectMediaResponse(ctx, waClient, keys, keys.MimeType, msg.ID, keys, params)
} }
func (wa *WhatsAppConnector) getDirectMediaMessageWithLIDSender(ctx context.Context, parsedID *waid.ParsedMediaID) (*database.Message, error) {
lid, err := wa.DeviceStore.LIDMap.GetLIDForPN(ctx, parsedID.Message.Sender)
if err != nil {
return nil, fmt.Errorf("failed to get LID for sender: %w", err)
} else if lid.IsEmpty() {
return nil, nil
}
return wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, waid.MakeMessageID(parsedID.Message.Chat, lid, parsedID.Message.ID))
}
func (wa *WhatsAppConnector) makeDirectMediaResponse( func (wa *WhatsAppConnector) makeDirectMediaResponse(
ctx context.Context, ctx context.Context,
waClient *WhatsAppClient, waClient *WhatsAppClient,

View file

@ -1,156 +0,0 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2026 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package connector
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"go.mau.fi/whatsmeow"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)
// wrapConversionError turns a failure to convert a Matrix event into a message status. Nothing has been
// sent to WhatsApp at that point, so the failure is certain.
func wrapConversionError(err error) error {
if err == nil || errors.As(err, &bridgev2.MessageStatus{}) {
return err
}
return bridgev2.WrapErrorInStatus(err).
WithMessage("failed to convert message for WhatsApp").
WithIsCertain(true).
WithSendNotice(true)
}
// wrapWhatsAppError turns an error from a whatsmeow request made while handling a Matrix event into a
// message status.
func wrapWhatsAppError(err error) error {
if err == nil || errors.As(err, &bridgev2.MessageStatus{}) {
return err
}
var iqErr *whatsmeow.IQError
var disconnectErr *whatsmeow.DisconnectedError
switch {
case errors.Is(err, whatsmeow.ErrNotConnected), errors.Is(err, whatsmeow.ErrClientIsNil):
return bridgev2.WrapErrorInStatus(err).
WithMessage("not connected to WhatsApp").
WithIsCertain(true).
WithSendNotice(true)
case errors.Is(err, whatsmeow.ErrMessageTimedOut), errors.Is(err, whatsmeow.ErrIQTimedOut),
errors.Is(err, context.DeadlineExceeded), errors.As(err, &disconnectErr):
return bridgev2.WrapErrorInStatus(err).
WithMessage("no response from WhatsApp").
WithIsCertain(false).
WithSendNotice(true)
case errors.Is(err, whatsmeow.ErrIQRateOverLimit):
return bridgev2.WrapErrorInStatus(err).
WithMessage("rate limited by WhatsApp").
WithErrorReason(event.MessageStatusNetworkError).
WithIsCertain(true).
WithSendNotice(true)
case errors.Is(err, whatsmeow.ErrIQForbidden), errors.Is(err, whatsmeow.ErrIQNotAuthorized):
return bridgev2.WrapErrorInStatus(err).
WithMessage("you don't have permission to do that on WhatsApp").
WithErrorReason(event.MessageStatusNoPermission).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
case errors.Is(err, whatsmeow.ErrServerReturnedError), errors.As(err, &iqErr):
return bridgev2.WrapErrorInStatus(err).
WithMessage("WhatsApp returned an error").
WithIsCertain(true).
WithSendNotice(true)
default:
return bridgev2.WrapErrorInStatus(err).
WithMessage("unexpected error from WhatsApp").
WithSendNotice(true)
}
}
// wrapSendError turns an error from sending a message to WhatsApp into a message status.
func (wa *WhatsAppClient) wrapSendError(err error) error {
switch sendErrorCode(err) {
case 403:
return bridgev2.WrapErrorInStatus(err).
WithMessage("you don't have permission to do that on WhatsApp").
WithErrorReason(event.MessageStatusNoPermission).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
case 463:
msg := "WhatsApp has temporarily restricted your account from messaging new contacts"
if until := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil; until.After(time.Now()) {
msg = fmt.Sprintf("WhatsApp has restricted your account from messaging new contacts until %s", until.UTC().Format("Jan 2, 2006 at 3:04 PM MST"))
}
return bridgev2.WrapErrorInStatus(err).
WithMessage(msg).
WithErrorReason(event.MessageStatusNoPermission).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
case 421, 479:
return bridgev2.WrapErrorInStatus(err).
WithMessage("WhatsApp rejected the message").
WithErrorReason(event.MessageStatusNetworkError).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
default:
return wrapWhatsAppError(err)
}
}
// sendErrorCode returns the code WhatsApp rejected a message with, which whatsmeow only includes in the
// error text.
func sendErrorCode(err error) int {
if !errors.Is(err, whatsmeow.ErrServerReturnedError) {
return 0
}
_, codeStr, _ := strings.Cut(err.Error(), whatsmeow.ErrServerReturnedError.Error()+" ")
code, _ := strconv.Atoi(codeStr)
return code
}
// wrapWhatsAppRespError turns an error from a whatsmeow request made during login or provisioning into
// an HTTP error, keeping the original error in the chain. Clients retry 429 and 502-504 automatically,
// which isn't wanted for provisioning requests, so those statuses aren't used.
func wrapWhatsAppRespError(err error) error {
var mapped bridgev2.RespError
var disconnectErr *whatsmeow.DisconnectedError
switch {
case errors.Is(err, whatsmeow.ErrIQRateOverLimit):
mapped = bridgev2.RespError(mautrix.MLimitExceeded.WithMessage("Rate limited by WhatsApp. Try again later.").WithStatus(http.StatusBadRequest))
case errors.Is(err, whatsmeow.ErrNotConnected), errors.Is(err, whatsmeow.ErrClientIsNil), errors.As(err, &disconnectErr):
mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("Not connected to WhatsApp"))
case errors.Is(err, whatsmeow.ErrIQTimedOut), errors.Is(err, context.DeadlineExceeded):
mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("WhatsApp didn't respond in time"))
case errors.Is(err, whatsmeow.ErrIQForbidden), errors.Is(err, whatsmeow.ErrIQNotAuthorized):
mapped = bridgev2.RespError(mautrix.MForbidden.WithMessage("WhatsApp denied the request"))
default:
mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("Request to WhatsApp failed"))
}
return fmt.Errorf("%w: %w", mapped.WithInternalError(err), err)
}

View file

@ -474,9 +474,7 @@ func (evt *WAMediaRetry) getRealSender() types.JID {
func (evt *WAMediaRetry) GetSender() bridgev2.EventSender { func (evt *WAMediaRetry) GetSender() bridgev2.EventSender {
realSender := pickLID(evt.getRealSender(), evt.senderLID) realSender := pickLID(evt.getRealSender(), evt.senderLID)
sender := evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender) return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender)
sender.ForceEditOrigSender = true
return sender
} }
func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID { func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID {

View file

@ -69,7 +69,7 @@ var (
func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (result *bridgev2.MatrixMessageResponse, retErr error) { func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (result *bridgev2.MatrixMessageResponse, retErr error) {
waMsg, optionMap, err := wa.Main.MsgConv.PollStartToWhatsApp(ctx, msg.Content, msg.ReplyTo, msg.Portal) waMsg, optionMap, err := wa.Main.MsgConv.PollStartToWhatsApp(ctx, msg.Content, msg.ReplyTo, msg.Portal)
if err != nil { if err != nil {
return nil, wrapConversionError(fmt.Errorf("failed to convert poll start: %w", err)) return nil, fmt.Errorf("failed to convert poll vote: %w", err)
} }
defer wa.mcTrack(msg, time.Now(), &retErr) defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil) resp, err := wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
@ -89,7 +89,7 @@ func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridge
func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (result *bridgev2.MatrixMessageResponse, retErr error) { func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (result *bridgev2.MatrixMessageResponse, retErr error) {
waMsg, err := wa.Main.MsgConv.PollVoteToWhatsApp(ctx, wa.Client, msg.Content, msg.VoteTo) waMsg, err := wa.Main.MsgConv.PollVoteToWhatsApp(ctx, wa.Client, msg.Content, msg.VoteTo)
if err != nil { if err != nil {
return nil, wrapConversionError(fmt.Errorf("failed to convert poll vote: %w", err)) return nil, fmt.Errorf("failed to convert poll vote: %w", err)
} }
defer wa.mcTrack(msg, time.Now(), &retErr) defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil) return wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
@ -98,17 +98,15 @@ func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev
func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (result *bridgev2.MatrixMessageResponse, retErr error) { func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (result *bridgev2.MatrixMessageResponse, retErr error) {
waMsg, req, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, msg.Event, msg.Content, msg.ReplyTo, msg.ThreadRoot, msg.Portal) waMsg, req, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, msg.Event, msg.Content, msg.ReplyTo, msg.ThreadRoot, msg.Portal)
if err != nil { if err != nil {
return nil, wrapConversionError(fmt.Errorf("failed to convert message: %w", err)) return nil, fmt.Errorf("failed to convert message: %w", err)
} }
defer wa.mcTrack(msg, time.Now(), &retErr) defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.handleConvertedMatrixMessage(ctx, msg, waMsg, req) return wa.handleConvertedMatrixMessage(ctx, msg, waMsg, req)
} }
var ErrBroadcastSendDisabled = bridgev2.WrapErrorInStatus(errors.New("sending status messages is disabled")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) var ErrBroadcastSendDisabled = bridgev2.WrapErrorInStatus(errors.New("sending status messages is disabled")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported)
var ErrBroadcastReactionUnsupported = bridgev2.WrapErrorInStatus(errors.New("reacting to status messages is not currently supported")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) var ErrBroadcastReactionUnsupported = bridgev2.WrapErrorInStatus(errors.New("reacting to status messages is not currently supported")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported)
var ErrNotCommunityAdmin = bridgev2.WrapErrorInStatus(errors.New("only community admins can send announcements")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusNoPermission).WithStatus(event.MessageStatusFail) var ErrNotCommunityAdmin = bridgev2.WrapErrorInStatus(errors.New("only community admins can send announcements")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusNoPermission)
var ErrEmptyGroupName = bridgev2.WrapErrorInStatus(errors.New("WhatsApp group names can't be empty")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail)
var ErrAddBlockedByPrivacy = bridgev2.WrapErrorInStatus(errors.New("this user's privacy settings don't allow adding them to groups")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusNoPermission).WithStatus(event.MessageStatusFail)
func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage, waMsg *waE2E.Message, req *whatsmeow.SendRequestExtra) (*bridgev2.MatrixMessageResponse, error) { func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage, waMsg *waE2E.Message, req *whatsmeow.SendRequestExtra) (*bridgev2.MatrixMessageResponse, error) {
if req == nil { if req == nil {
@ -141,7 +139,7 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
if errors.Is(err, whatsmeow.ErrNoPermissionInGroup) { if errors.Is(err, whatsmeow.ErrNoPermissionInGroup) {
return nil, ErrNotCommunityAdmin return nil, ErrNotCommunityAdmin
} }
return nil, wa.wrapSendError(err) return nil, err
} }
var pickedMessageID networkid.MessageID var pickedMessageID networkid.MessageID
if resp.Sender == wa.GetLID() { if resp.Sender == wa.GetLID() {
@ -206,7 +204,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup { if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(ctx, wa.Client, messageID), reactionMsg.ReactionMessage) reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(ctx, wa.Client, messageID), reactionMsg.ReactionMessage)
if err != nil { if err != nil {
return nil, wrapWhatsAppError(fmt.Errorf("failed to encrypt reaction: %w", err)) return nil, fmt.Errorf("failed to encrypt reaction: %w", err)
} }
reactionMsg.ReactionMessage = nil reactionMsg.ReactionMessage = nil
req.Meta = &types.MsgMetaInfo{ req.Meta = &types.MsgMetaInfo{
@ -220,7 +218,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
Metadata: &waid.ReactionMetadata{ Metadata: &waid.ReactionMetadata{
SenderDeviceID: wa.JID.Device, SenderDeviceID: wa.JID.Device,
}, },
}, wa.wrapSendError(err) }, err
} }
func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) (retErr error) { func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) (retErr error) {
@ -250,7 +248,7 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b
defer wa.mcTrack(msg, time.Now(), &retErr) defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra) resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra)
zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response") zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response")
return wa.wrapSendError(err) return err
} }
func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) (retErr error) { func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) (retErr error) {
@ -275,7 +273,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M
waMsg, _, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, edit.Event, edit.Content, nil, nil, edit.Portal) waMsg, _, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, edit.Event, edit.Content, nil, nil, edit.Portal)
if err != nil { if err != nil {
return wrapConversionError(fmt.Errorf("failed to convert message: %w", err)) return fmt.Errorf("failed to convert message: %w", err)
} }
defer wa.mcTrack(edit, time.Now(), &retErr) defer wa.mcTrack(edit, time.Now(), &retErr)
@ -290,7 +288,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M
ID: editID, ID: editID,
}) })
log.Trace().Any("response", resp).Msg("WhatsApp edit response") log.Trace().Any("response", resp).Msg("WhatsApp edit response")
return wa.wrapSendError(err) return err
} }
func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) (retErr error) { func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) (retErr error) {
@ -315,11 +313,11 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
resp, err := wa.Client.SendMessage(ctx, portalJID, revokeMessage, extra) resp, err := wa.Client.SendMessage(ctx, portalJID, revokeMessage, extra)
log.Trace().Any("response", resp).Msg("WhatsApp delete response") log.Trace().Any("response", resp).Msg("WhatsApp delete response")
return wa.wrapSendError(err) return err
} }
func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) { func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) {
if receipt.Receipt.IsThread() || !receipt.ReadUpTo.After(receipt.LastRead) { if !receipt.ReadUpTo.After(receipt.LastRead) {
return nil return nil
} }
if receipt.LastRead.IsZero() { if receipt.LastRead.IsZero() {
@ -364,7 +362,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
log.Err(err).Strs("ids", ids).Msg("Failed to mark messages as read") log.Err(err).Strs("ids", ids).Msg("Failed to mark messages as read")
} }
} }
return wrapWhatsAppError(err) return err
} }
func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) (retErr error) { func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) (retErr error) {
@ -395,7 +393,7 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set presence on typing") zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set presence on typing")
} }
} }
return wrapWhatsAppError(wa.Client.SendChatPresence(ctx, portalJID, chatPresence, mediaPresence)) return wa.Client.SendChatPresence(ctx, portalJID, chatPresence, mediaPresence)
} }
var errUnsupportedDisappearingTimer = bridgev2.WrapErrorInStatus(errors.New("invalid value for disappearing timer")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true) var errUnsupportedDisappearingTimer = bridgev2.WrapErrorInStatus(errors.New("invalid value for disappearing timer")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true)
@ -416,7 +414,7 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
settingTS := time.UnixMilli(msg.Event.Timestamp) settingTS := time.UnixMilli(msg.Event.Timestamp)
err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS) err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS)
if err != nil { if err != nil {
return false, wrapWhatsAppError(err) return false, err
} }
msg.Portal.Metadata.(*waid.PortalMetadata).DisappearingTimerSetAt = settingTS.Unix() msg.Portal.Metadata.(*waid.PortalMetadata).DisappearingTimerSetAt = settingTS.Unix()
msg.Portal.Disappear = database.DisappearingSetting{ msg.Portal.Disappear = database.DisappearingSetting{
@ -451,7 +449,7 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
return nil, fmt.Errorf("cannot change channel members") return nil, fmt.Errorf("cannot change channel members")
} }
defer wa.mcTrack(msg, time.Now(), &retErr) defer wa.mcTrack(msg, time.Now(), &retErr)
return nil, wrapWhatsAppError(wa.Client.UnfollowNewsletter(ctx, portalJID)) return nil, wa.Client.UnfollowNewsletter(ctx, portalJID)
} }
changes := make([]types.JID, 1) changes := make([]types.JID, 1)
@ -482,11 +480,9 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
defer wa.mcTrack(msg, time.Now(), &retErr) defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action) resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action)
if err != nil { if err != nil {
return nil, wrapWhatsAppError(err) return nil, err
} else if len(resp) == 0 { } else if len(resp) == 0 {
return nil, fmt.Errorf("no response for participant change") return nil, fmt.Errorf("no response for participant change")
} else if resp[0].Error == 403 && action == whatsmeow.ParticipantChangeAdd {
return nil, ErrAddBlockedByPrivacy
} else if resp[0].Error != 0 { } else if resp[0].Error != 0 {
return nil, fmt.Errorf("failed to change participant: code %d", resp[0].Error) return nil, fmt.Errorf("failed to change participant: code %d", resp[0].Error)
} }
@ -505,14 +501,12 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev
if msg.Portal.RoomType == database.RoomTypeDM { if msg.Portal.RoomType == database.RoomTypeDM {
return false, fmt.Errorf("cannot set room name for DM") return false, fmt.Errorf("cannot set room name for DM")
} else if msg.Content.Name == "" {
return false, ErrEmptyGroupName
} }
defer wa.mcTrack(msg, time.Now(), &retErr) defer wa.mcTrack(msg, time.Now(), &retErr)
err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name) err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name)
if err != nil { if err != nil {
return false, wrapWhatsAppError(err) return false, err
} }
msg.Portal.Name = msg.Content.Name msg.Portal.Name = msg.Content.Name
@ -536,7 +530,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge
oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID
err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic) err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic)
if err != nil { if err != nil {
return false, wrapWhatsAppError(err) return false, err
} }
msg.Portal.Topic = msg.Content.Topic msg.Portal.Topic = msg.Content.Topic
@ -572,7 +566,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridg
defer wa.mcTrack(msg, time.Now(), &retErr) defer wa.mcTrack(msg, time.Now(), &retErr)
avatarID, err := wa.Client.SetGroupPhoto(ctx, portalJID, data) avatarID, err := wa.Client.SetGroupPhoto(ctx, portalJID, data)
if err != nil { if err != nil {
return false, wrapWhatsAppError(err) return false, err
} }
msg.Portal.AvatarMXC = msg.Content.URL msg.Portal.AvatarMXC = msg.Content.URL
@ -601,7 +595,7 @@ func convertRoomAvatar(data []byte) ([]byte, error) {
if isCorrectSize && imageType == "jpeg" { if isCorrectSize && imageType == "jpeg" {
return data, nil return data, nil
} else if len(data) > 10*1024*1024 || width > 12000 || height > 12000 { } else if len(data) > 10*1024*1024 || width > 12000 || height > 12000 {
return nil, fmt.Errorf("%w: avatar is too large for re-encoding", bridgev2.ErrMediaTooLarge) return nil, fmt.Errorf("avatar is too large for re-encoding")
} }
img, _, err := image.Decode(bytes.NewReader(data)) img, _, err := image.Decode(bytes.NewReader(data))
@ -709,7 +703,7 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg
} }
if chatJID.Server == types.NewsletterServer { if chatJID.Server == types.NewsletterServer {
defer wa.mcTrack(msg, time.Now(), &retErr) defer wa.mcTrack(msg, time.Now(), &retErr)
return wrapWhatsAppError(wa.Client.UnfollowNewsletter(ctx, chatJID)) return wa.Client.UnfollowNewsletter(ctx, chatJID)
} else if chatJID.Server == types.GroupServer { } else if chatJID.Server == types.GroupServer {
memberInfo, err := wa.Main.Bridge.Matrix.GetMemberInfo(ctx, msg.Portal.MXID, wa.UserLogin.UserMXID) memberInfo, err := wa.Main.Bridge.Matrix.GetMemberInfo(ctx, msg.Portal.MXID, wa.UserLogin.UserMXID)
if err != nil { if err != nil {
@ -718,7 +712,7 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg
err = wa.Client.LeaveGroup(ctx, chatJID) err = wa.Client.LeaveGroup(ctx, chatJID)
if err != nil { if err != nil {
// TODO ignore errors saying you already left the group? // TODO ignore errors saying you already left the group?
return wrapWhatsAppError(fmt.Errorf("failed to leave group before deleting chat: %w", err)) return fmt.Errorf("failed to leave group before deleting chat: %w", err)
} }
} }
} }
@ -727,7 +721,7 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg
return err return err
} }
defer wa.mcTrack(msg, time.Now(), &retErr) defer wa.mcTrack(msg, time.Now(), &retErr)
return wrapWhatsAppError(wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true))) return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true))
} }
func (wa *WhatsAppClient) HandleMatrixBlockUser(ctx context.Context, msg *bridgev2.MatrixBlockUser) error { func (wa *WhatsAppClient) HandleMatrixBlockUser(ctx context.Context, msg *bridgev2.MatrixBlockUser) error {
@ -739,7 +733,7 @@ func (wa *WhatsAppClient) HandleMatrixBlockUser(ctx context.Context, msg *bridge
return err return err
} }
if msg.Content.ReportSpam { if msg.Content.ReportSpam {
return bridgev2.WrapErrorInStatus(errors.New("reporting spam is not supported on WhatsApp")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) return fmt.Errorf("spam reporting is not supported")
} }
action := events.BlocklistChangeActionUnblock action := events.BlocklistChangeActionUnblock
if msg.Content.Block { if msg.Content.Block {
@ -759,5 +753,5 @@ func (wa *WhatsAppClient) HandleMatrixBlockUser(ctx context.Context, msg *bridge
} }
}() }()
} }
return wrapWhatsAppError(err) return err
} }

View file

@ -237,11 +237,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
}) })
wa.notifyOfflineSyncWaiter(fmt.Errorf("stream error: %s", message)) wa.notifyOfflineSyncWaiter(fmt.Errorf("stream error: %s", message))
case *events.StreamReplaced: case *events.StreamReplaced:
wa.UserLogin.BridgeState.Send(status.BridgeState{ wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAStreamReplaced})
StateEvent: status.StateUnknownError,
Error: WAStreamReplaced,
BlockAutoReconnect: true,
})
wa.notifyOfflineSyncWaiter(fmt.Errorf("stream replaced")) wa.notifyOfflineSyncWaiter(fmt.Errorf("stream replaced"))
case *events.KeepAliveTimeout: case *events.KeepAliveTimeout:
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WAKeepaliveTimeout}) wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WAKeepaliveTimeout})
@ -257,17 +253,12 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
wa.notifyOfflineSyncWaiter(fmt.Errorf("connection failure: %s (%s)", evt.Reason, evt.Message)) wa.notifyOfflineSyncWaiter(fmt.Errorf("connection failure: %s (%s)", evt.Reason, evt.Message))
case *events.ClientOutdated: case *events.ClientOutdated:
wa.UserLogin.Log.Error().Msg("Got a client outdated connect failure. The bridge is likely out of date, please update immediately.") wa.UserLogin.Log.Error().Msg("Got a client outdated connect failure. The bridge is likely out of date, please update immediately.")
wa.UserLogin.BridgeState.Send(status.BridgeState{ wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAClientOutdated})
StateEvent: status.StateUnknownError,
Error: WAClientOutdated,
BlockAutoReconnect: true,
})
wa.notifyOfflineSyncWaiter(fmt.Errorf("client outdated")) wa.notifyOfflineSyncWaiter(fmt.Errorf("client outdated"))
case *events.TemporaryBan: case *events.TemporaryBan:
wa.UserLogin.BridgeState.Send(status.BridgeState{ wa.UserLogin.BridgeState.Send(status.BridgeState{
StateEvent: status.StateBadCredentials, StateEvent: status.StateBadCredentials,
Error: WATemporaryBan, Error: WATemporaryBan,
UserAction: status.UserActionRestart,
Message: evt.String(), Message: evt.String(),
}) })
wa.notifyOfflineSyncWaiter(fmt.Errorf("temporary ban: %s", evt.String())) wa.notifyOfflineSyncWaiter(fmt.Errorf("temporary ban: %s", evt.String()))
@ -374,14 +365,6 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD { if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD {
parentKey := messageAssoc.GetParentMessageKey() parentKey := messageAssoc.GetParentMessageKey()
protocolMsg, shouldHideEdit := makeHDMediaReplacementEdit(evt.Message, parentKey) protocolMsg, shouldHideEdit := makeHDMediaReplacementEdit(evt.Message, parentKey)
if protocolMsg.GetEditedMessage() == nil {
wa.UserLogin.Log.Debug().
Str("message_id", evt.Info.ID).
Str("parent_id", parentKey.GetID()).
Stringer("assoc_type", assocType).
Msg("Ignoring HD replacement message without replacement content")
return
}
dontRenderEdited = shouldHideEdit dontRenderEdited = shouldHideEdit
wa.UserLogin.Log.Debug(). wa.UserLogin.Log.Debug().
Str("message_id", evt.Info.ID). Str("message_id", evt.Info.ID).
@ -530,10 +513,6 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
} else if evt.Chat.Server == types.NewsletterServer { } else if evt.Chat.Server == types.NewsletterServer {
messageSender = evt.Chat messageSender = evt.Chat
} }
var messageSenderAlt types.JID
if evt.MessageSender.IsEmpty() && evt.Chat.Server == types.BotServer {
messageSenderAlt = wa.JID
}
var chatAlt types.JID var chatAlt types.JID
if evt.Chat.Server == types.DefaultUserServer { if evt.Chat.Server == types.DefaultUserServer {
chatLID, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Chat) chatLID, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Chat)
@ -547,9 +526,6 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
if !chatAlt.IsEmpty() { if !chatAlt.IsEmpty() {
targets = append(targets, waid.MakeMessageID(chatAlt, messageSender, id)) targets = append(targets, waid.MakeMessageID(chatAlt, messageSender, id))
} }
if !messageSenderAlt.IsEmpty() {
targets = append(targets, waid.MakeMessageID(evt.Chat, messageSenderAlt, id))
}
} }
senderLID := evt.Sender senderLID := evt.Sender
if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() { if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() {
@ -737,27 +713,6 @@ func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.De
Type: bridgev2.RemoteEventChatDelete, Type: bridgev2.RemoteEventChatDelete,
PortalKey: wa.makeWAPortalKey(chatJID), PortalKey: wa.makeWAPortalKey(chatJID),
Timestamp: evt.Timestamp, Timestamp: evt.Timestamp,
PreHandleWithResultFunc: func(ctx context.Context, portal *bridgev2.Portal) *bridgev2.EventHandlingResult {
messageRange := evt.Action.GetMessageRange()
deletedAt := max(
evt.Timestamp.Unix(),
messageRange.GetLastMessageTimestamp(),
messageRange.GetLastSystemMessageTimestamp(),
)
if deletedAt <= 0 {
return nil
}
lastMessage, err := wa.Main.Bridge.DB.Message.GetLastNInPortal(ctx, portal.PortalKey, 1)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get last message to check if chat delete is outdated")
} else if len(lastMessage) == 1 && lastMessage[0].Timestamp.Unix() > deletedAt {
zerolog.Ctx(ctx).Debug().
Time("last_message_ts", lastMessage[0].Timestamp).
Msg("Ignoring chat delete that is older than the last message")
return ptr.Ptr(bridgev2.EventHandlingResultIgnored)
}
return nil
},
}, },
OnlyForMe: true, OnlyForMe: true,
Children: true, Children: true,
@ -978,9 +933,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt
defer wa.appStateRecoveryLock.Unlock() defer wa.appStateRecoveryLock.Unlock()
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata) meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
if ts, exists := meta.AppStateRecoveryAttempted[evt.Name]; exists { if ts, exists := meta.AppStateRecoveryAttempted[evt.Name]; exists {
if !evt.Recovery { delete(wa.appStateFullSyncAttempted, evt.Name)
delete(wa.appStateFullSyncAttempted, evt.Name)
}
delete(meta.AppStateRecoveryAttempted, evt.Name) delete(meta.AppStateRecoveryAttempted, evt.Name)
err := wa.UserLogin.Save(ctx) err := wa.UserLogin.Save(ctx)
if err != nil { if err != nil {
@ -1018,7 +971,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev
return return
} }
if !evt.FullSync { if !evt.FullSync {
if !lastFullSync.IsZero() && time.Since(lastFullSync) < 48*time.Hour { if !lastFullSync.IsZero() {
log.Debug(). log.Debug().
Err(evt.Error). Err(evt.Error).
Time("last_full_sync_attempt", lastFullSync). Time("last_full_sync_attempt", lastFullSync).

View file

@ -18,7 +18,6 @@ package connector
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
@ -26,11 +25,9 @@ import (
"go.mau.fi/util/dbutil" "go.mau.fi/util/dbutil"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-whatsapp/pkg/waid" "go.mau.fi/mautrix-whatsapp/pkg/waid"
) )
@ -50,8 +47,6 @@ func (wa *WhatsAppClient) FindAltTargetMessage(ctx context.Context, targetMsg ne
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else if parsed.Chat == types.NewMetaAIJID {
altChat = types.MetaAIJID
} }
if !altSender.IsEmpty() { if !altSender.IsEmpty() {
altSenderID := *parsed altSenderID := *parsed
@ -63,8 +58,6 @@ func (wa *WhatsAppClient) FindAltTargetMessage(ctx context.Context, targetMsg ne
altChatID.Chat = altChat altChatID.Chat = altChat
if altSender.Server == types.DefaultUserServer { if altSender.Server == types.DefaultUserServer {
altChatID.Sender = altSender altChatID.Sender = altSender
} else if parsed.Sender == types.NewMetaAIJID {
altChatID.Sender = types.MetaAIJID
} }
alts = append(alts, altChatID.String()) alts = append(alts, altChatID.String())
} }
@ -218,30 +211,6 @@ func (wa *WhatsAppConnector) migrateToLIDDMs(ctx context.Context) error {
return nil return nil
} }
func (wa *WhatsAppConnector) migrateMetaAIPortals(ctx context.Context) error {
if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_meta_ai_portals_migrated") == "true" {
return nil
}
log := zerolog.Ctx(ctx).With().Str("action", "migrate meta ai portals").Logger()
const findMetaAIPortals = "SELECT id, receiver FROM portal WHERE bridge_id=$1 AND id=$2"
portalKeys, err := scanPortalKey.NewRowIter(wa.Bridge.DB.Query(ctx, findMetaAIPortals, wa.Bridge.ID, waid.MakePortalID(types.MetaAIJID))).AsList()
if err != nil {
return fmt.Errorf("failed to get Meta AI portals: %w", err)
}
for _, key := range portalKeys {
res, err := wa.reIDPhoneDMToLID(ctx, types.MetaAIJID, types.NewMetaAIJID, key.Receiver)
if err != nil {
return fmt.Errorf("failed to re-ID Meta AI portal for %s: %w", key.Receiver, err)
}
log.Info().
Str("receiver", string(key.Receiver)).
Stringer("result", res).
Msg("Re-ID'd Meta AI phone number portal")
}
wa.Bridge.DB.KV.Set(ctx, "whatsapp_meta_ai_portals_migrated", "true")
return nil
}
func (wa *WhatsAppConnector) syncMismatchingGhosts(ctx context.Context) error { func (wa *WhatsAppConnector) syncMismatchingGhosts(ctx context.Context) error {
if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_avatars_resynced") == "true" { if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_avatars_resynced") == "true" {
return nil return nil
@ -298,84 +267,3 @@ func (wa *WhatsAppConnector) syncMismatchingGhosts(ctx context.Context) error {
wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_avatars_resynced", "true") wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_avatars_resynced", "true")
return nil return nil
} }
func (wa *WhatsAppConnector) removeRejoinedPNGhosts(ctx context.Context) error {
if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dm_pn_ghosts_removed_v2") == "true" {
return nil
}
stateGetter, ok := wa.Bridge.Matrix.(bridgev2.MatrixConnectorWithArbitraryRoomState)
if !ok {
return nil
}
log := zerolog.Ctx(ctx).With().Str("action", "remove rejoined pn ghosts").Logger()
const findLIDPortals = "SELECT id, mxid FROM portal WHERE bridge_id=$1 AND room_type='dm' AND id LIKE '%@lid' AND mxid IS NOT NULL"
lidPortals, err := scanLIDPortal.NewRowIter(wa.Bridge.DB.Query(ctx, findLIDPortals, wa.Bridge.ID)).AsList()
if err != nil {
return fmt.Errorf("failed to get LID portals: %w", err)
}
var removed, failed int
for _, portal := range lidPortals {
lid, err := waid.ParsePortalID(portal.ID)
if err != nil || lid.Server != types.HiddenUserServer {
continue
}
pn, err := wa.DeviceStore.LIDMap.GetPNForLID(ctx, lid)
if err != nil {
return fmt.Errorf("failed to get PN for LID portal %s: %w", portal.ID, err)
} else if pn.IsEmpty() {
continue
}
pnUserID := waid.MakeUserID(pn)
memberEvt, err := stateGetter.GetStateEvent(ctx, portal.MXID, event.StateMember, wa.Bridge.Matrix.FormatGhostMXID(pnUserID).String())
if errors.Is(err, mautrix.MNotFound) || errors.Is(err, mautrix.MForbidden) {
continue
} else if err != nil {
log.Warn().Err(err).Stringer("room_id", portal.MXID).Msg("Failed to get PN ghost membership")
failed++
continue
} else if memberEvt == nil || memberEvt.Content.AsMember().Membership != event.MembershipJoin {
continue
}
pnGhost, err := wa.Bridge.GetGhostByID(ctx, pnUserID)
if err != nil {
return fmt.Errorf("failed to get PN ghost for %s: %w", pn, err)
}
_, err = pnGhost.Intent.SendState(ctx, portal.MXID, event.StateMember, pnGhost.Intent.GetMXID().String(), &event.Content{
Parsed: &event.MemberEventContent{Membership: event.MembershipLeave, Reason: "Migrating to LIDs"},
Raw: map[string]any{"com.beeper.exclude_from_timeline": true},
}, time.Time{})
if err != nil {
log.Err(err).Stringer("room_id", portal.MXID).Stringer("pn", pn).Msg("Failed to remove PN ghost from LID DM")
if !errors.Is(err, mautrix.MForbidden) && !errors.Is(err, mautrix.MNotFound) {
failed++
}
continue
}
log.Debug().
Stringer("room_id", portal.MXID).
Stringer("pn", pn).
Stringer("join_sender", memberEvt.Sender).
Time("join_ts", time.UnixMilli(memberEvt.Timestamp)).
Msg("Removed PN ghost from LID DM")
removed++
}
log.Info().
Int("removed_ghosts", removed).
Int("failed", failed).
Int("total_lid_portals", len(lidPortals)).
Msg("Finished removing rejoined PN ghosts from LID DMs")
if failed == 0 && ctx.Err() == nil {
wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dm_pn_ghosts_removed_v2", "true")
}
return nil
}
type lidPortal struct {
ID networkid.PortalID
MXID id.RoomID
}
var scanLIDPortal = dbutil.ConvertRowFn[lidPortal](func(row dbutil.Scannable) (portal lidPortal, err error) {
err = row.Scan(&portal.ID, &portal.MXID)
return
})

View file

@ -137,7 +137,6 @@ var (
_ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil) _ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil)
_ bridgev2.LoginProcessUserInput = (*WALogin)(nil) _ bridgev2.LoginProcessUserInput = (*WALogin)(nil)
_ bridgev2.LoginProcessWithOverride = (*WALogin)(nil) _ bridgev2.LoginProcessWithOverride = (*WALogin)(nil)
_ bridgev2.LoginProcessWithParams = (*WALogin)(nil)
_ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil) _ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil)
) )
@ -145,14 +144,6 @@ const LoginConnectWait = 30 * time.Second
const LoginPairPhoneWait = 30 * time.Second const LoginPairPhoneWait = 30 * time.Second
func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
return wl.StartWithParams(ctx, bridgev2.LoginStartParams{})
}
func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogin) (*bridgev2.LoginStep, error) {
return wl.StartWithParams(ctx, bridgev2.LoginStartParams{Override: old})
}
func (wl *WALogin) StartWithParams(ctx context.Context, params bridgev2.LoginStartParams) (*bridgev2.LoginStep, error) {
wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect) wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect)
device := wl.Main.DeviceStore.NewDevice() device := wl.Main.DeviceStore.NewDevice()
wl.Client = whatsmeow.NewClient(device, waLog.Zerolog(wl.Log)) wl.Client = whatsmeow.NewClient(device, waLog.Zerolog(wl.Log))
@ -162,20 +153,8 @@ func (wl *WALogin) StartWithParams(ctx context.Context, params bridgev2.LoginSta
if err := wl.Main.updateProxy(ctx, wl.Client, true); err != nil { if err := wl.Main.updateProxy(ctx, wl.Client, true); err != nil {
return nil, err return nil, err
} }
if params.WebSocketHTTP != nil {
wl.Client.SetPreLoginHTTPClient(&http.Client{Transport: params.WebSocketHTTP})
}
if wl.PhoneCode { if wl.PhoneCode {
if params.Override != nil {
phoneNumber := fmt.Sprintf("+%s", params.Override.ID)
wl.Log.Debug().
Str("phone_number", phoneNumber).
Msg("Auto-submitting phone number for relogin")
return wl.SubmitUserInput(ctx, map[string]string{
"phone_number": phoneNumber,
})
}
return &bridgev2.LoginStep{ return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeUserInput, Type: bridgev2.LoginStepTypeUserInput,
StepID: LoginStepIDPhoneNumber, StepID: LoginStepIDPhoneNumber,
@ -192,7 +171,7 @@ func (wl *WALogin) StartWithParams(ctx context.Context, params bridgev2.LoginSta
err := wl.Client.Connect() err := wl.Client.Connect()
if err != nil { if err != nil {
wl.Log.Err(err).Msg("Failed to connect to WhatsApp for QR login") wl.Log.Err(err).Msg("Failed to connect to WhatsApp for QR login")
return nil, wrapWhatsAppRespError(err) return nil, err
} }
ctx, cancel := context.WithTimeout(ctx, LoginConnectWait) ctx, cancel := context.WithTimeout(ctx, LoginConnectWait)
defer cancel() defer cancel()
@ -205,11 +184,25 @@ func (wl *WALogin) StartWithParams(ctx context.Context, params bridgev2.LoginSta
return makeQRStep(wl.QRs[0]), nil return makeQRStep(wl.QRs[0]), nil
} }
func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogin) (*bridgev2.LoginStep, error) {
step, err := wl.Start(ctx)
if err == nil && step != nil && old != nil && step.StepID == LoginStepIDPhoneNumber {
phoneNumber := fmt.Sprintf("+%s", old.ID)
wl.Log.Debug().
Str("phone_number", phoneNumber).
Msg("Auto-submitting phone number for relogin")
return wl.SubmitUserInput(ctx, map[string]string{
"phone_number": phoneNumber,
})
}
return step, err
}
func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) {
err := wl.Client.Connect() err := wl.Client.Connect()
if err != nil { if err != nil {
wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login") wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login")
return nil, wrapWhatsAppRespError(err) return nil, err
} }
connectCtx, cancelConnect := context.WithTimeout(ctx, LoginConnectWait) connectCtx, cancelConnect := context.WithTimeout(ctx, LoginConnectWait)
err = wl.WaitForQRs.Wait(connectCtx) err = wl.WaitForQRs.Wait(connectCtx)
@ -230,7 +223,7 @@ func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string)
} else if errors.Is(err, whatsmeow.ErrIQRateOverLimit) { } else if errors.Is(err, whatsmeow.ErrIQRateOverLimit) {
return nil, ErrRateLimitedByWhatsApp return nil, ErrRateLimitedByWhatsApp
} }
return nil, wrapWhatsAppRespError(err) return nil, err
} }
wl.Log.Debug().Msg("Phone code login started") wl.Log.Debug().Msg("Phone code login started")
return &bridgev2.LoginStep{ return &bridgev2.LoginStep{
@ -288,7 +281,6 @@ func (wl *WALogin) handleEvent(rawEvt any) {
wl.QRs[i] = strings.Replace(code, evt.OldSecret, evt.NewSecret, 1) wl.QRs[i] = strings.Replace(code, evt.OldSecret, evt.NewSecret, 1)
} }
wl.ADVRotate.Set() wl.ADVRotate.Set()
return
case *events.QRScannedWithoutMultidevice: case *events.QRScannedWithoutMultidevice:
wl.Log.Error().Msg("QR code scanned without multidevice enabled") wl.Log.Error().Msg("QR code scanned without multidevice enabled")
wl.LoginError = ErrLoginMultideviceNotEnabled wl.LoginError = ErrLoginMultideviceNotEnabled
@ -324,7 +316,6 @@ func (wl *WALogin) handleEvent(rawEvt any) {
wl.LoginError = ErrUnexpectedDisconnect wl.LoginError = ErrUnexpectedDisconnect
case *events.ManualLoginReconnect: case *events.ManualLoginReconnect:
wl.Received515.Set() wl.Received515.Set()
return
default: default:
wl.Log.Warn().Type("event_type", evt).Msg("Got unexpected event") wl.Log.Warn().Type("event_type", evt).Msg("Got unexpected event")
return return
@ -373,14 +364,9 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
case <-wl.PasskeyRequest.GetChan(): case <-wl.PasskeyRequest.GetChan():
return wl.makePasskeyStep() return wl.makePasskeyStep()
case <-wl.ADVRotate.GetChan(): case <-wl.ADVRotate.GetChan():
wl.Log.Debug().Msg("ADV secret was rotated, returning new QR immediately")
wl.ADVRotate.Clear() wl.ADVRotate.Clear()
if currentIndex < 0 || currentIndex >= len(wl.QRs) { return makeQRStep(wl.QRs[nextIndex]), nil
wl.Log.Debug().Msg("ADV secret was rotated, but no QRs are left to return")
wl.Cancel()
return nil, ErrLoginTimeout
}
wl.Log.Debug().Msg("ADV secret was rotated, returning current QR immediately")
return makeQRStep(wl.QRs[currentIndex]), nil
case <-wl.LoginComplete.GetChan(): case <-wl.LoginComplete.GetChan():
// continue // continue
} }
@ -475,7 +461,7 @@ func (wl *WALogin) SubmitWebAuthnResponse(ctx context.Context, rawResp json.RawM
if err != nil { if err != nil {
wl.Log.Err(err).Msg("Failed to send WebAuthn response") wl.Log.Err(err).Msg("Failed to send WebAuthn response")
wl.Cancel() wl.Cancel()
return nil, wrapWhatsAppRespError(err) return nil, err
} }
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -494,7 +480,7 @@ func (wl *WALogin) SubmitWebAuthnResponse(ctx context.Context, rawResp json.RawM
if err != nil { if err != nil {
wl.Log.Err(err).Msg("Failed to send WebAuthn confirmation") wl.Log.Err(err).Msg("Failed to send WebAuthn confirmation")
wl.Cancel() wl.Cancel()
return nil, wrapWhatsAppRespError(err) return nil, err
} }
case <-wl.LoginComplete.GetChan(): case <-wl.LoginComplete.GetChan():
} }

Some files were not shown because too many files have changed in this diff Show more