mirror of
https://github.com/mautrix/whatsapp.git
synced 2026-11-10 16:19:18 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a15089b18d |
||
|
|
945474343f |
||
|
|
5c8c1fc835 |
||
|
|
d9b0daca89 |
||
|
|
ac29fa5272 |
||
|
|
d574a67a2d |
||
|
|
5640ef311b |
||
|
|
81f64557ea | ||
|
|
03859bb3db |
||
|
|
1d024590d2 |
||
|
|
5ba69ab4ee |
||
|
|
429320b373 |
||
|
|
78c5ee3b20 |
||
|
|
e097ba38de |
||
|
|
4be45ddbea |
||
|
|
96ce0799be |
||
|
|
07d8e9417b |
||
|
|
3a5d6448a8 |
||
|
|
9f82246069 | ||
|
|
5dd621fdf4 |
||
|
|
de02399ff8 |
||
|
|
ae8dcdec4b | ||
|
|
678d4ebb4f | ||
|
|
1f30d5516c | ||
|
|
e8f1d2b3f6 |
||
|
|
123ac4f340 |
||
|
|
33357ced5a |
||
|
|
ab1fae7181 |
||
|
|
7a0b4b5808 |
||
|
|
88f5fc738c |
||
|
|
51b5c5e5fb |
||
|
|
021504afb0 |
||
|
|
76c59bd05d |
||
|
|
bb09b7584c |
||
|
|
870414b015 |
||
|
|
57e055c3ad |
||
|
|
322485c5a9 |
||
|
|
960b4ed000 | ||
|
|
2759bc5ea2 | ||
|
|
1678681af7 |
||
|
|
56a19a1d10 |
||
|
|
8f1937b259 |
||
|
|
83f6cca39b |
||
|
|
33b7768ef0 |
||
|
|
c8564186f4 |
||
|
|
c998fd8858 |
||
|
|
054a7dee77 |
||
|
|
46afb8f781 |
||
|
|
c8e38f1dc5 |
||
|
|
1067291a89 | ||
|
|
1f7131c2ee |
||
|
|
0932968106 | ||
|
|
abf1940af8 |
||
|
|
73b70f2753 | ||
|
|
bf98cabad2 |
||
|
|
7fb697fb89 |
||
|
|
1fc894948c |
||
|
|
45d51e3905 |
||
|
|
be8d93edc4 |
||
|
|
aa86329e37 |
||
|
|
977389392c |
||
|
|
5f904a6203 |
||
|
|
9959878fe0 | ||
|
|
cb7d14e979 | ||
|
|
a34259020e |
41 changed files with 1296 additions and 251 deletions
1
.bridgelint.yaml
Normal file
1
.bridgelint.yaml
Normal file
|
|
@ -0,0 +1 @@
|
|||
code_format_allowlist: ["wa-connect-failure-%d"]
|
||||
1
.github/workflows/go.yml
vendored
1
.github/workflows/go.yml
vendored
|
|
@ -30,6 +30,7 @@ jobs:
|
|||
run: |
|
||||
go install golang.org/x/tools/cmd/goimports@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"
|
||||
|
||||
- name: Run pre-commit
|
||||
|
|
|
|||
|
|
@ -21,9 +21,12 @@ repos:
|
|||
- id: go-mod-tidy
|
||||
|
||||
- repo: https://github.com/beeper/pre-commit-go
|
||||
rev: v0.4.2
|
||||
rev: v0.5.0
|
||||
hooks:
|
||||
- id: zerolog-ban-msgf
|
||||
- id: zerolog-use-stringer
|
||||
- id: prevent-literal-http-methods
|
||||
- id: zerolog-ban-global-log
|
||||
- id: bridgelint
|
||||
language: system
|
||||
verbose: true
|
||||
|
|
|
|||
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -1,3 +1,28 @@
|
|||
# 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
|
||||
|
||||
* Bumped minimum Go version to 1.26.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ var m = mxmain.BridgeMain{
|
|||
Name: "mautrix-whatsapp",
|
||||
URL: "https://github.com/mautrix/whatsapp",
|
||||
Description: "A Matrix-WhatsApp puppeting bridge.",
|
||||
Version: "26.09",
|
||||
Version: "26.10",
|
||||
SemCalVer: true,
|
||||
Connector: &connector.WhatsAppConnector{},
|
||||
}
|
||||
|
|
|
|||
28
go.mod
28
go.mod
|
|
@ -2,7 +2,7 @@ module go.mau.fi/mautrix-whatsapp
|
|||
|
||||
go 1.26.0
|
||||
|
||||
toolchain go1.27.1
|
||||
toolchain go1.27.2
|
||||
|
||||
tool go.mau.fi/util/cmd/maubuild
|
||||
|
||||
|
|
@ -10,17 +10,17 @@ require (
|
|||
github.com/google/uuid v1.6.0
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/tidwall/gjson v1.19.0
|
||||
github.com/tidwall/gjson v1.20.0
|
||||
github.com/yuin/goldmark v1.8.6
|
||||
go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8
|
||||
go.mau.fi/util v0.11.0
|
||||
go.mau.fi/webp v0.3.0
|
||||
go.mau.fi/whatsmeow v0.0.0-20260929112325-8b41cfe6d9c4
|
||||
golang.org/x/image v0.46.0
|
||||
golang.org/x/net v0.59.0
|
||||
golang.org/x/sync v0.23.0
|
||||
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a
|
||||
golang.org/x/image v0.47.0
|
||||
golang.org/x/net v0.61.0
|
||||
golang.org/x/sync v0.24.0
|
||||
google.golang.org/protobuf v1.36.12
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
maunium.net/go/mautrix v0.31.1-0.20260929190113-4aac2bbf2888
|
||||
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -33,7 +33,7 @@ require (
|
|||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.52 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // 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
|
||||
go.mau.fi/libsignal v0.2.2 // indirect
|
||||
go.mau.fi/zeroconfig v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/crypto v0.58.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e // indirect
|
||||
golang.org/x/mod v0.42.0 // indirect
|
||||
golang.org/x/sys v0.49.0 // indirect
|
||||
golang.org/x/text v0.43.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
maunium.net/go/mauflag v1.0.0 // indirect
|
||||
|
|
|
|||
52
go.sum
52
go.sum
|
|
@ -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-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
|
||||
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 h1:lcWAnrqr2nNfDiArwFNHCE4787Mw2tCdVSOXCru0/0E=
|
||||
github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b h1:OzNsuVdSWGwXvWKTtChx9ve89k4cFJ6NxYM4Aw4/f+E=
|
||||
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
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.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/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
||||
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||
github.com/tidwall/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto=
|
||||
github.com/tidwall/gjson v1.20.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||
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/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=
|
||||
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/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcUkqKshE+n239UtbbJOn4=
|
||||
go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc=
|
||||
go.mau.fi/util v0.11.0 h1:JNEpkVRxfTdJUC0DIGKiRlLKzl4G/qLX/eVzoP7QuwY=
|
||||
go.mau.fi/util v0.11.0/go.mod h1:04AEFVsc4zNdIkDC1gkdqbg/nZFolHY50pyXh2DSy7s=
|
||||
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/whatsmeow v0.0.0-20260929112325-8b41cfe6d9c4 h1:0PXBB/t9JjRLCyv8Dp5K1OyE+ut/6VgKZqGUsaAaPNk=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260929112325-8b41cfe6d9c4/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4=
|
||||
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a h1:ZyCV/mWfRkutxs5R0K3v+oyTbF4huprSlPZg/LEFQ7k=
|
||||
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a/go.mod h1:iY4lm61nTLWsNiIE1EmHUZwhKGBeoV4dhnVE2uMtNoQ=
|
||||
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
|
||||
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU=
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E=
|
||||
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e h1:HgI6W1OolKK6tcV74is+rl9SMT0njsHlp0TbQiGXI/A=
|
||||
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e/go.mod h1:DBoxwWhjaW8DJJw/YeM4zpeOKMC1FMXSblsYy+1mFSw=
|
||||
golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo=
|
||||
golang.org/x/image v0.47.0/go.mod h1:IkN5o4A7vq55vXN8FWjjnhg9Rs2GOtffqsSi9mWAKYE=
|
||||
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
|
||||
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
|
||||
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
|
||||
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
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=
|
||||
|
|
@ -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=
|
||||
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/mautrix v0.31.1-0.20260929190113-4aac2bbf2888 h1:sC0fpamjHE5Q2nqy9frxv30rHcX53xPg0FRZ3C2IVoU=
|
||||
maunium.net/go/mautrix v0.31.1-0.20260929190113-4aac2bbf2888/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk=
|
||||
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445 h1:ZI9Y2YEmjqQDloOCiciHeynTRYjUwK0ZZbNcRH/5NOc=
|
||||
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU=
|
||||
|
|
|
|||
|
|
@ -142,11 +142,20 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
|
|||
Str("peer_data_request_session_id", evt.GetPeerDataRequestSessionID()).
|
||||
Msg("Downloading history sync")
|
||||
blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true)
|
||||
if err != nil {
|
||||
if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) {
|
||||
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")
|
||||
errored = true
|
||||
return
|
||||
}
|
||||
if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND {
|
||||
wa.finishOnDemandHistorySyncRequest(evt.GetPeerDataRequestSessionID())
|
||||
wa.handleOnDemandHistorySync(ctx, blob)
|
||||
if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil {
|
||||
log.Err(err).Msg("Failed to delete queued on-demand history sync notification")
|
||||
|
|
@ -170,6 +179,7 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
|
|||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to store history sync notification data")
|
||||
errored = true
|
||||
} else {
|
||||
resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP ||
|
||||
blob.GetSyncType() == waHistorySync.HistorySync_RECENT ||
|
||||
|
|
@ -245,10 +255,17 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
} else if jid.Server == types.BroadcastServer {
|
||||
log.Debug().Stringer("chat_jid", jid).Msg("Skipping broadcast list in history sync")
|
||||
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 {
|
||||
totalMessageCount += len(conv.GetMessages())
|
||||
}
|
||||
if jid.Server == types.DefaultUserServer {
|
||||
if jid == types.MetaAIJID {
|
||||
jid = types.NewMetaAIJID
|
||||
} else if jid.Server == types.DefaultUserServer {
|
||||
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid)
|
||||
if err != nil {
|
||||
log.Err(err).Stringer("pn", jid).Msg("Failed to get LID for phone number in history sync")
|
||||
|
|
@ -276,11 +293,17 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
for i, rawMsg := range rawMessages {
|
||||
// Don't store messages that will just be skipped.
|
||||
msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage())
|
||||
if err != nil {
|
||||
if err != nil && getMessageType(rawMsg.GetMessage().GetMessage()) == "ignore" {
|
||||
ignoredTypes++
|
||||
continue
|
||||
} else if err != nil {
|
||||
log.Warn().Err(err).
|
||||
Int("msg_index", i).
|
||||
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
|
||||
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")
|
||||
log.Trace().
|
||||
Any("web_message_info", rawMsg.GetMessage()).
|
||||
|
|
@ -434,7 +457,7 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
|||
return
|
||||
}
|
||||
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true)
|
||||
if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) {
|
||||
if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) || errors.Is(err, ErrNewsletterNotFound) {
|
||||
log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID).
|
||||
Msg("Skipping creating room because the user is not a participant")
|
||||
//err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID)
|
||||
|
|
@ -585,16 +608,36 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
|||
}
|
||||
}
|
||||
}
|
||||
resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true)
|
||||
resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true, !params.Forward && !hasMore)
|
||||
if err != nil {
|
||||
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.Forward = params.Forward
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
var err error
|
||||
var rows int64
|
||||
|
|
@ -627,6 +670,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
|
|||
portalJID types.JID,
|
||||
messages []*waWeb.WebMessageInfo,
|
||||
explodeOnError bool,
|
||||
deleteEverything bool,
|
||||
) (*bridgev2.FetchMessagesResponse, error) {
|
||||
cli := wa.Client
|
||||
if cli == nil {
|
||||
|
|
@ -663,6 +707,9 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
|
|||
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, false) {
|
||||
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
|
||||
converted, mediaReq := wa.convertHistorySyncMessage(
|
||||
ctx, cli, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
|
||||
|
|
@ -682,9 +729,11 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
|
|||
Messages: convertedMessages,
|
||||
Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)),
|
||||
CompleteCallback: func() {
|
||||
// TODO this only deletes after backfilling. If there's no need for backfill after a relogin,
|
||||
// the messages will be stuck in the database
|
||||
wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS)
|
||||
if deleteEverything {
|
||||
wa.deleteHistorySyncMessages(ctx, portalJID, 1<<62, oldestTS)
|
||||
} else {
|
||||
wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS)
|
||||
}
|
||||
if len(mediaRequests) > 0 {
|
||||
go func(ctx context.Context) {
|
||||
for _, req := range mediaRequests {
|
||||
|
|
@ -706,12 +755,28 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID
|
|||
if params.AnchorMessage == nil {
|
||||
return nil, fmt.Errorf("anchor message is required to fetch messages from phone")
|
||||
}
|
||||
parsed, err := waid.ParseMessageID(params.AnchorMessage.ID)
|
||||
anchor := params.AnchorMessage
|
||||
parsed, err := waid.ParseMessageID(anchor.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse anchor message ID: %w", err)
|
||||
anchor, err = wa.Main.Bridge.DB.Message.GetFirstPortalMessageExcludingIDPattern(ctx, params.Portal.PortalKey, "fake:%")
|
||||
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()
|
||||
if !wa.startOnDemandHistorySyncRequest(ctx, portalJID, msgID) {
|
||||
return &bridgev2.FetchMessagesResponse{
|
||||
HasMore: true,
|
||||
Pending: true,
|
||||
}, nil
|
||||
}
|
||||
reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{
|
||||
MessageSource: types.MessageSource{
|
||||
Chat: portalJID,
|
||||
|
|
@ -720,7 +785,7 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID
|
|||
IsGroup: parsed.Chat.Server == types.GroupServer,
|
||||
},
|
||||
ID: parsed.ID,
|
||||
Timestamp: params.AnchorMessage.Timestamp,
|
||||
Timestamp: anchor.Timestamp,
|
||||
}, 50)
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Str("request_msg_id", msgID).
|
||||
|
|
@ -733,6 +798,9 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID
|
|||
Peer: true,
|
||||
})
|
||||
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 &bridgev2.FetchMessagesResponse{
|
||||
|
|
@ -741,6 +809,38 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID
|
|||
}, 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) {
|
||||
if len(blob.GetConversations()) > 1 {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
|
|
@ -753,7 +853,9 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w
|
|||
zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID")
|
||||
continue
|
||||
}
|
||||
if portalJID.Server == types.DefaultUserServer {
|
||||
if portalJID == types.MetaAIJID {
|
||||
portalJID = types.NewMetaAIJID
|
||||
} else if portalJID.Server == types.DefaultUserServer {
|
||||
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, portalJID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("lid", portalJID).Msg("Failed to get LID for phone number in on-demand history sync")
|
||||
|
|
@ -794,7 +896,7 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w
|
|||
Int("message_count", len(messages)).
|
||||
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()).
|
||||
Msg("Converting messages to bridge from on-demand history sync")
|
||||
resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false)
|
||||
resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
|
|||
}
|
||||
|
||||
func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) {
|
||||
return 1, 9
|
||||
return 1, 10
|
||||
}
|
||||
|
||||
const WAMaxFileSize = 2000 * 1024 * 1024
|
||||
|
|
@ -67,7 +67,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
|
|||
}
|
||||
|
||||
func capID() string {
|
||||
base := "fi.mau.whatsapp.capabilities.2026_07_22"
|
||||
base := "fi.mau.whatsapp.capabilities.2026_10_01"
|
||||
if ffmpeg.Supported() {
|
||||
return base + "+ffmpeg"
|
||||
}
|
||||
|
|
@ -198,6 +198,7 @@ var whatsappCaps = &event.RoomFeatures{
|
|||
|
||||
var whatsappDMCaps *event.RoomFeatures
|
||||
var whatsappCAGCaps *event.RoomFeatures
|
||||
var whatsappNoteToSelfCaps *event.RoomFeatures
|
||||
|
||||
func init() {
|
||||
whatsappDMCaps = ptr.Clone(whatsappCaps)
|
||||
|
|
@ -205,7 +206,13 @@ func init() {
|
|||
whatsappDMCaps.State = event.StateFeatureMap{
|
||||
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
|
||||
}
|
||||
whatsappDMCaps.BlockUser = true
|
||||
whatsappDMCaps.MemberActions = nil
|
||||
|
||||
whatsappNoteToSelfCaps = ptr.Clone(whatsappDMCaps)
|
||||
whatsappNoteToSelfCaps.ID = capID() + "+self"
|
||||
whatsappNoteToSelfCaps.BlockUser = false
|
||||
|
||||
whatsappCAGCaps = ptr.Clone(whatsappCaps)
|
||||
whatsappCAGCaps.ID = capID() + "+cag"
|
||||
whatsappCAGCaps.Reply = event.CapLevelUnsupported
|
||||
|
|
@ -216,6 +223,10 @@ func (wa *WhatsAppClient) GetCapabilities(ctx context.Context, portal *bridgev2.
|
|||
if portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
|
||||
return whatsappCAGCaps
|
||||
} else if portal.RoomType == database.RoomTypeDM {
|
||||
jid, err := waid.ParsePortalID(portal.ID)
|
||||
if err == nil && wa.IsOwnJID(jid) {
|
||||
return whatsappNoteToSelfCaps
|
||||
}
|
||||
return whatsappDMCaps
|
||||
}
|
||||
return whatsappCaps
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/appstate"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
|
|
@ -30,6 +31,7 @@ func (wa *WhatsAppClient) GetChatInfo(ctx context.Context, portal *bridgev2.Port
|
|||
}
|
||||
|
||||
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) {
|
||||
switch portalJID.Server {
|
||||
|
|
@ -55,6 +57,8 @@ func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID,
|
|||
info, err := wa.Client.GetNewsletterInfo(ctx, portalJID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if info == nil || info.ID.IsEmpty() {
|
||||
return nil, ErrNewsletterNotFound
|
||||
}
|
||||
wrapped = wa.wrapNewsletterInfo(ctx, info)
|
||||
default:
|
||||
|
|
@ -116,9 +120,16 @@ func (wa *WhatsAppClient) applyChatSettings(ctx context.Context, chatID types.JI
|
|||
return
|
||||
}
|
||||
}
|
||||
info.UserLocal = &bridgev2.UserLocalPortalInfo{
|
||||
MutedUntil: ptr.Ptr(chat.MutedUntil),
|
||||
wa.appStateRecoveryLock.Lock()
|
||||
_, muteStateBroken := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).AppStateRecoveryAttempted[appstate.WAPatchRegularHigh]
|
||||
wa.appStateRecoveryLock.Unlock()
|
||||
userLocal := &bridgev2.UserLocalPortalInfo{}
|
||||
if !muteStateBroken {
|
||||
userLocal.MutedUntil = ptr.Ptr(chat.MutedUntil)
|
||||
} else if info.UserLocal != nil {
|
||||
userLocal.MutedUntil = info.UserLocal.MutedUntil
|
||||
}
|
||||
info.UserLocal = userLocal
|
||||
if chat.Pinned {
|
||||
info.UserLocal.Tag = ptr.Ptr(wa.Main.Config.PinnedTag)
|
||||
} else if chat.Archived {
|
||||
|
|
@ -185,6 +196,11 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
|
|||
},
|
||||
ExcludeChangesFromTimeline: true,
|
||||
}
|
||||
var err error
|
||||
info.UserBlocked, err = wa.isUserBlocked(ctx, jid)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get blocked state")
|
||||
}
|
||||
if jid.Server == types.BotServer {
|
||||
info.Topic = ptr.Ptr(BotChatTopic)
|
||||
}
|
||||
|
|
@ -263,6 +279,8 @@ func setTopicID(id, topic string) bridgev2.ExtraUpdater[*bridgev2.Portal] {
|
|||
|
||||
func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupInfo) *bridgev2.ChatInfo {
|
||||
sendEventPL := defaultPL
|
||||
// Community announcement groups allow non-admins to send threaded replies.
|
||||
// Power levels can't represent that, but the power level is checked separately when handling messages.
|
||||
if info.IsAnnounce && !info.IsDefaultSubGroup {
|
||||
sendEventPL = adminPL
|
||||
}
|
||||
|
|
@ -293,6 +311,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
|
|||
event.StateRoomName: metaChangePL,
|
||||
event.StateRoomAvatar: metaChangePL,
|
||||
event.StateTopic: metaChangePL,
|
||||
event.EventEncrypted: defaultPL, // Need to always allow encrypted events for poll responses
|
||||
event.EventReaction: defaultPL,
|
||||
event.EventRedaction: defaultPL,
|
||||
event.EventUnstablePollResponse: defaultPL,
|
||||
|
|
@ -559,15 +578,32 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
|
|||
StateDefault: ptr.Ptr(nobodyPL),
|
||||
Ban: ptr.Ptr(nobodyPL),
|
||||
Events: map[event.Type]int{
|
||||
event.StateRoomName: adminPL,
|
||||
event.StateRoomAvatar: adminPL,
|
||||
event.StateTopic: adminPL,
|
||||
event.EventReaction: defaultPL,
|
||||
event.EventRedaction: defaultPL,
|
||||
event.EventUnstablePollResponse: defaultPL,
|
||||
event.StateRoomName: adminPL,
|
||||
event.StateRoomAvatar: adminPL,
|
||||
event.StateTopic: adminPL,
|
||||
event.EventReaction: defaultPL,
|
||||
event.EventRedaction: defaultPL,
|
||||
// TODO support poll votes in channels
|
||||
//event.EventUnstablePollResponse: defaultPL,
|
||||
},
|
||||
},
|
||||
},
|
||||
Type: ptr.Ptr(database.RoomTypeDefault),
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) isUserBlocked(ctx context.Context, jid types.JID) (*bool, error) {
|
||||
wa.blocklistLock.Lock()
|
||||
defer wa.blocklistLock.Unlock()
|
||||
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
|
||||
blocklist := meta.Blocklist
|
||||
if blocklist == nil {
|
||||
runHandlers := wa.resyncBlocklistUnlocked(ctx)
|
||||
go runHandlers()
|
||||
blocklist = meta.Blocklist
|
||||
}
|
||||
if blocklist == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return new(blocklist.IsBlocked(jid)), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
|
|||
mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle),
|
||||
pushNamesSynced: exsync.NewEvent(),
|
||||
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,
|
||||
}
|
||||
|
|
@ -82,6 +85,7 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
|
|||
w.Client.ManualHistorySyncDownload = true
|
||||
w.Client.SendReportingTokens = true
|
||||
w.Client.AutomaticMessageRerequestFromPhone = true
|
||||
w.Client.UnifiedSessionInterval = 24 * time.Hour
|
||||
w.Client.GetMessageForRetry = w.trackNotFoundRetry
|
||||
w.Client.PreRetryCallback = w.trackFoundRetry
|
||||
w.Client.BackgroundEventCtx = w.UserLogin.Log.WithContext(wa.Bridge.BackgroundCtx)
|
||||
|
|
@ -122,6 +126,7 @@ type WhatsAppClient struct {
|
|||
isNewLogin bool
|
||||
pushNamesSynced *exsync.Event
|
||||
lastPresence types.Presence
|
||||
blocklistLock sync.Mutex
|
||||
|
||||
disableNewsletter bool
|
||||
|
||||
|
|
@ -131,6 +136,12 @@ type WhatsAppClient struct {
|
|||
wasaResyncLock sync.Mutex
|
||||
museProfileLock sync.Mutex
|
||||
lastMuseProfileRequest time.Time
|
||||
|
||||
onDemandHistorySyncLock sync.Mutex
|
||||
onDemandHistorySyncRequests map[types.JID]onDemandHistorySyncRequest
|
||||
|
||||
previousOwnLIDs *exsync.Set[types.JID]
|
||||
previousOwnLIDsLock sync.Mutex
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -199,7 +210,26 @@ func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID)
|
|||
|
||||
func (wa *WhatsAppClient) IsOwnJID(jid types.JID) bool {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -148,13 +148,7 @@ func fnSync(ce *commands.Event) {
|
|||
case "appstate":
|
||||
names := appstate.AllPatchNames[:]
|
||||
if len(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
|
||||
})
|
||||
names = parseAppStateNames(ce, ce.Args[1:])
|
||||
}
|
||||
for _, name := range names {
|
||||
err := wa.Client.FetchAppState(ce.Ctx, name, true, false)
|
||||
|
|
@ -174,6 +168,50 @@ 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{
|
||||
Func: fnInviteLink,
|
||||
Name: "invite-link",
|
||||
|
|
|
|||
|
|
@ -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.MsgConv.DB = wa.DB
|
||||
wa.Bridge.Commands.(*commands.Processor).AddHandlers(
|
||||
cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin,
|
||||
cmdAccept, cmdSync, cmdResetAppState, cmdInviteLink, cmdResolveLink, cmdJoin,
|
||||
)
|
||||
wa.mediaEditCache = make(MediaEditCache)
|
||||
wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]()
|
||||
|
|
@ -164,11 +164,19 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error {
|
|||
if err != nil {
|
||||
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() {
|
||||
err = wa.syncMismatchingGhosts(wa.Bridge.BackgroundCtx)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
|
@ -235,7 +243,16 @@ func (wa *WhatsAppConnector) GenerateTransactionID(_ id.UserID, _ id.RoomID, _ e
|
|||
}
|
||||
|
||||
func (wa *WhatsAppConnector) ResetHTTPTransport() {
|
||||
// No-op for now, whatsmeow doesn't use the shared transport config yet
|
||||
for _, login := range wa.Bridge.GetAllCachedUserLogins() {
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import (
|
|||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waMmsRetry"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
|
|
@ -163,7 +164,19 @@ func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, par
|
|||
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)
|
||||
if err != nil {
|
||||
var httpErr whatsmeow.DownloadHTTPError
|
||||
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
|
||||
} else if sticker == nil {
|
||||
return nil, mautrix.MNotFound.WithMessage("Sticker not found in pack")
|
||||
|
|
@ -173,6 +186,9 @@ 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) {
|
||||
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 {
|
||||
return nil, fmt.Errorf("failed to get message: %w", err)
|
||||
} else if msg == nil {
|
||||
|
|
@ -215,6 +231,16 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
|
|||
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(
|
||||
ctx context.Context,
|
||||
waClient *WhatsAppClient,
|
||||
|
|
|
|||
156
pkg/connector/errors.go
Normal file
156
pkg/connector/errors.go
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
// 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)
|
||||
}
|
||||
|
|
@ -469,24 +469,14 @@ func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context {
|
|||
}
|
||||
|
||||
func (evt *WAMediaRetry) getRealSender() types.JID {
|
||||
sender := evt.SenderID
|
||||
if sender.IsEmpty() {
|
||||
if evt.FromMe {
|
||||
if evt.ChatID.Server == types.HiddenUserServer {
|
||||
sender = evt.wa.GetLID().ToNonAD()
|
||||
} else {
|
||||
sender = evt.wa.JID.ToNonAD()
|
||||
}
|
||||
} else if evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.HiddenUserServer || evt.ChatID.Server == types.BotServer {
|
||||
sender = evt.ChatID.ToNonAD()
|
||||
}
|
||||
}
|
||||
return sender
|
||||
return evt.wa.getRealSender(evt.ChatID, evt.SenderID, evt.FromMe)
|
||||
}
|
||||
|
||||
func (evt *WAMediaRetry) GetSender() bridgev2.EventSender {
|
||||
realSender := pickLID(evt.getRealSender(), evt.senderLID)
|
||||
return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender)
|
||||
sender := evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender)
|
||||
sender.ForceEditOrigSender = true
|
||||
return sender
|
||||
}
|
||||
|
||||
func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID {
|
||||
|
|
@ -551,6 +541,7 @@ func (evt *WAMediaRetry) ConvertEdit(ctx context.Context, portal *bridgev2.Porta
|
|||
log.Debug().Msg("Got error response in media retry notification, but response also contains a new download URL - trying to download")
|
||||
}
|
||||
}
|
||||
log.Trace().Any("retry_content", retryData).Msg("Decrypted successful media retry response")
|
||||
err = evt.wa.mediaRetryLock.Acquire(ctx, 1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to acquire media retry lock: %w", err)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,19 @@
|
|||
// 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 (
|
||||
|
|
@ -19,6 +35,7 @@ import (
|
|||
"go.mau.fi/whatsmeow/proto/waCommon"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
"golang.org/x/image/draw"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
|
|
@ -46,12 +63,13 @@ var (
|
|||
_ bridgev2.TagHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.MarkedUnreadHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.DeleteChatHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.UserBlockingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert poll vote: %w", err)
|
||||
return nil, wrapConversionError(fmt.Errorf("failed to convert poll start: %w", err))
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
resp, err := wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
|
||||
|
|
@ -71,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) {
|
||||
waMsg, err := wa.Main.MsgConv.PollVoteToWhatsApp(ctx, wa.Client, msg.Content, msg.VoteTo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert poll vote: %w", err)
|
||||
return nil, wrapConversionError(fmt.Errorf("failed to convert poll vote: %w", err))
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
return wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
|
||||
|
|
@ -80,14 +98,17 @@ func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev
|
|||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert message: %w", err)
|
||||
return nil, wrapConversionError(fmt.Errorf("failed to convert message: %w", err))
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
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)
|
||||
var ErrBroadcastReactionUnsupported = bridgev2.WrapErrorInStatus(errors.New("reacting to status messages is not currently supported")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported)
|
||||
var ErrBroadcastSendDisabled = bridgev2.WrapErrorInStatus(errors.New("sending status messages is disabled")).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).WithStatus(event.MessageStatusFail)
|
||||
var ErrNotCommunityAdmin = bridgev2.WrapErrorInStatus(errors.New("only community admins can send announcements")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusNoPermission).WithStatus(event.MessageStatusFail)
|
||||
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) {
|
||||
if req == nil {
|
||||
|
|
@ -98,6 +119,7 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
|
|||
} else {
|
||||
req.ID = wa.Client.GenerateMessageID()
|
||||
}
|
||||
req.CheckPermissions = true
|
||||
|
||||
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
|
|
@ -116,7 +138,10 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
|
|||
zerolog.Ctx(ctx).Trace().Any("payload", waMsg).Msg("Outgoing message payload")
|
||||
resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if errors.Is(err, whatsmeow.ErrNoPermissionInGroup) {
|
||||
return nil, ErrNotCommunityAdmin
|
||||
}
|
||||
return nil, wa.wrapSendError(err)
|
||||
}
|
||||
var pickedMessageID networkid.MessageID
|
||||
if resp.Sender == wa.GetLID() {
|
||||
|
|
@ -181,7 +206,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
|
|||
if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
|
||||
reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(ctx, wa.Client, messageID), reactionMsg.ReactionMessage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt reaction: %w", err)
|
||||
return nil, wrapWhatsAppError(fmt.Errorf("failed to encrypt reaction: %w", err))
|
||||
}
|
||||
reactionMsg.ReactionMessage = nil
|
||||
req.Meta = &types.MsgMetaInfo{
|
||||
|
|
@ -195,7 +220,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
|
|||
Metadata: &waid.ReactionMetadata{
|
||||
SenderDeviceID: wa.JID.Device,
|
||||
},
|
||||
}, err
|
||||
}, wa.wrapSendError(err)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) (retErr error) {
|
||||
|
|
@ -225,7 +250,7 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b
|
|||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra)
|
||||
zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response")
|
||||
return err
|
||||
return wa.wrapSendError(err)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) (retErr error) {
|
||||
|
|
@ -250,7 +275,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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert message: %w", err)
|
||||
return wrapConversionError(fmt.Errorf("failed to convert message: %w", err))
|
||||
}
|
||||
defer wa.mcTrack(edit, time.Now(), &retErr)
|
||||
|
||||
|
|
@ -265,7 +290,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M
|
|||
ID: editID,
|
||||
})
|
||||
log.Trace().Any("response", resp).Msg("WhatsApp edit response")
|
||||
return err
|
||||
return wa.wrapSendError(err)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) (retErr error) {
|
||||
|
|
@ -290,11 +315,11 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
|
|||
|
||||
resp, err := wa.Client.SendMessage(ctx, portalJID, revokeMessage, extra)
|
||||
log.Trace().Any("response", resp).Msg("WhatsApp delete response")
|
||||
return err
|
||||
return wa.wrapSendError(err)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) {
|
||||
if !receipt.ReadUpTo.After(receipt.LastRead) {
|
||||
if receipt.Receipt.IsThread() || !receipt.ReadUpTo.After(receipt.LastRead) {
|
||||
return nil
|
||||
}
|
||||
if receipt.LastRead.IsZero() {
|
||||
|
|
@ -339,7 +364,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
|
|||
log.Err(err).Strs("ids", ids).Msg("Failed to mark messages as read")
|
||||
}
|
||||
}
|
||||
return err
|
||||
return wrapWhatsAppError(err)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) (retErr error) {
|
||||
|
|
@ -370,7 +395,7 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.
|
|||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set presence on typing")
|
||||
}
|
||||
}
|
||||
return wa.Client.SendChatPresence(ctx, portalJID, chatPresence, mediaPresence)
|
||||
return wrapWhatsAppError(wa.Client.SendChatPresence(ctx, portalJID, chatPresence, mediaPresence))
|
||||
}
|
||||
|
||||
var errUnsupportedDisappearingTimer = bridgev2.WrapErrorInStatus(errors.New("invalid value for disappearing timer")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true)
|
||||
|
|
@ -391,7 +416,7 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
|
|||
settingTS := time.UnixMilli(msg.Event.Timestamp)
|
||||
err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, wrapWhatsAppError(err)
|
||||
}
|
||||
msg.Portal.Metadata.(*waid.PortalMetadata).DisappearingTimerSetAt = settingTS.Unix()
|
||||
msg.Portal.Disappear = database.DisappearingSetting{
|
||||
|
|
@ -421,6 +446,12 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
|
|||
default:
|
||||
return nil, nil
|
||||
}
|
||||
} else if portalJID.Server == types.NewsletterServer {
|
||||
if msg.Type != bridgev2.Leave {
|
||||
return nil, fmt.Errorf("cannot change channel members")
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
return nil, wrapWhatsAppError(wa.Client.UnfollowNewsletter(ctx, portalJID))
|
||||
}
|
||||
|
||||
changes := make([]types.JID, 1)
|
||||
|
|
@ -451,9 +482,11 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
|
|||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, wrapWhatsAppError(err)
|
||||
} else if len(resp) == 0 {
|
||||
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 {
|
||||
return nil, fmt.Errorf("failed to change participant: code %d", resp[0].Error)
|
||||
}
|
||||
|
|
@ -472,12 +505,14 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev
|
|||
|
||||
if msg.Portal.RoomType == database.RoomTypeDM {
|
||||
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)
|
||||
err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, wrapWhatsAppError(err)
|
||||
}
|
||||
|
||||
msg.Portal.Name = msg.Content.Name
|
||||
|
|
@ -501,7 +536,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge
|
|||
oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID
|
||||
err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, wrapWhatsAppError(err)
|
||||
}
|
||||
|
||||
msg.Portal.Topic = msg.Content.Topic
|
||||
|
|
@ -537,7 +572,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridg
|
|||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
avatarID, err := wa.Client.SetGroupPhoto(ctx, portalJID, data)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, wrapWhatsAppError(err)
|
||||
}
|
||||
|
||||
msg.Portal.AvatarMXC = msg.Content.URL
|
||||
|
|
@ -566,7 +601,7 @@ func convertRoomAvatar(data []byte) ([]byte, error) {
|
|||
if isCorrectSize && imageType == "jpeg" {
|
||||
return data, nil
|
||||
} else if len(data) > 10*1024*1024 || width > 12000 || height > 12000 {
|
||||
return nil, fmt.Errorf("avatar is too large for re-encoding")
|
||||
return nil, fmt.Errorf("%w: avatar is too large for re-encoding", bridgev2.ErrMediaTooLarge)
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
|
|
@ -672,7 +707,10 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if chatJID.Server == types.GroupServer {
|
||||
if chatJID.Server == types.NewsletterServer {
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
return wrapWhatsAppError(wa.Client.UnfollowNewsletter(ctx, chatJID))
|
||||
} else if chatJID.Server == types.GroupServer {
|
||||
memberInfo, err := wa.Main.Bridge.Matrix.GetMemberInfo(ctx, msg.Portal.MXID, wa.UserLogin.UserMXID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get own member info: %w", err)
|
||||
|
|
@ -680,7 +718,7 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg
|
|||
err = wa.Client.LeaveGroup(ctx, chatJID)
|
||||
if err != nil {
|
||||
// TODO ignore errors saying you already left the group?
|
||||
return fmt.Errorf("failed to leave group before deleting chat: %w", err)
|
||||
return wrapWhatsAppError(fmt.Errorf("failed to leave group before deleting chat: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -689,5 +727,37 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg
|
|||
return err
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true))
|
||||
return wrapWhatsAppError(wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true)))
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixBlockUser(ctx context.Context, msg *bridgev2.MatrixBlockUser) error {
|
||||
if wa.Client == nil {
|
||||
return bridgev2.ErrNotLoggedIn
|
||||
}
|
||||
jid, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
}
|
||||
action := events.BlocklistChangeActionUnblock
|
||||
if msg.Content.Block {
|
||||
action = events.BlocklistChangeActionBlock
|
||||
}
|
||||
blocklist, err := wa.Client.UpdateBlocklist(ctx, jid, action, "")
|
||||
if err == nil {
|
||||
wa.blocklistLock.Lock()
|
||||
defer wa.blocklistLock.Unlock()
|
||||
evts := wa.saveBlocklistUnlocked(ctx, blocklist)
|
||||
go func() {
|
||||
for _, evt := range evts {
|
||||
if evt.PortalKey == msg.Portal.PortalKey {
|
||||
continue
|
||||
}
|
||||
wa.UserLogin.QueueRemoteEvent(evt)
|
||||
}
|
||||
}()
|
||||
}
|
||||
return wrapWhatsAppError(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
|||
|
||||
success = true
|
||||
switch evt := rawEvt.(type) {
|
||||
case *events.Blocklist:
|
||||
success = wa.resyncBlocklist(ctx)()
|
||||
case *events.Message:
|
||||
success = wa.handleWAMessage(ctx, evt)
|
||||
case *events.Receipt:
|
||||
|
|
@ -235,7 +237,11 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
|||
})
|
||||
wa.notifyOfflineSyncWaiter(fmt.Errorf("stream error: %s", message))
|
||||
case *events.StreamReplaced:
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAStreamReplaced})
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{
|
||||
StateEvent: status.StateUnknownError,
|
||||
Error: WAStreamReplaced,
|
||||
BlockAutoReconnect: true,
|
||||
})
|
||||
wa.notifyOfflineSyncWaiter(fmt.Errorf("stream replaced"))
|
||||
case *events.KeepAliveTimeout:
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WAKeepaliveTimeout})
|
||||
|
|
@ -251,12 +257,17 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
|||
wa.notifyOfflineSyncWaiter(fmt.Errorf("connection failure: %s (%s)", evt.Reason, evt.Message))
|
||||
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.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAClientOutdated})
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{
|
||||
StateEvent: status.StateUnknownError,
|
||||
Error: WAClientOutdated,
|
||||
BlockAutoReconnect: true,
|
||||
})
|
||||
wa.notifyOfflineSyncWaiter(fmt.Errorf("client outdated"))
|
||||
case *events.TemporaryBan:
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{
|
||||
StateEvent: status.StateBadCredentials,
|
||||
Error: WATemporaryBan,
|
||||
UserAction: status.UserActionRestart,
|
||||
Message: evt.String(),
|
||||
})
|
||||
wa.notifyOfflineSyncWaiter(fmt.Errorf("temporary ban: %s", evt.String()))
|
||||
|
|
@ -363,6 +374,14 @@ 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 {
|
||||
parentKey := messageAssoc.GetParentMessageKey()
|
||||
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
|
||||
wa.UserLogin.Log.Debug().
|
||||
Str("message_id", evt.Info.ID).
|
||||
|
|
@ -511,6 +530,10 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
|
|||
} else if evt.Chat.Server == types.NewsletterServer {
|
||||
messageSender = evt.Chat
|
||||
}
|
||||
var messageSenderAlt types.JID
|
||||
if evt.MessageSender.IsEmpty() && evt.Chat.Server == types.BotServer {
|
||||
messageSenderAlt = wa.JID
|
||||
}
|
||||
var chatAlt types.JID
|
||||
if evt.Chat.Server == types.DefaultUserServer {
|
||||
chatLID, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Chat)
|
||||
|
|
@ -524,6 +547,9 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
|
|||
if !chatAlt.IsEmpty() {
|
||||
targets = append(targets, waid.MakeMessageID(chatAlt, messageSender, id))
|
||||
}
|
||||
if !messageSenderAlt.IsEmpty() {
|
||||
targets = append(targets, waid.MakeMessageID(evt.Chat, messageSenderAlt, id))
|
||||
}
|
||||
}
|
||||
senderLID := evt.Sender
|
||||
if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() {
|
||||
|
|
@ -711,6 +737,27 @@ func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.De
|
|||
Type: bridgev2.RemoteEventChatDelete,
|
||||
PortalKey: wa.makeWAPortalKey(chatJID),
|
||||
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,
|
||||
Children: true,
|
||||
|
|
@ -719,7 +766,7 @@ func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.De
|
|||
|
||||
func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool {
|
||||
chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID)
|
||||
senderJID := wa.maybeConvertJIDToLID(ctx, evt.SenderJID)
|
||||
senderJID := wa.getRealSender(chatJID, wa.maybeConvertJIDToLID(ctx, evt.SenderJID), evt.IsFromMe)
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventMessageRemove,
|
||||
|
|
@ -931,7 +978,9 @@ func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt
|
|||
defer wa.appStateRecoveryLock.Unlock()
|
||||
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
|
||||
if ts, exists := meta.AppStateRecoveryAttempted[evt.Name]; exists {
|
||||
delete(wa.appStateFullSyncAttempted, evt.Name)
|
||||
if !evt.Recovery {
|
||||
delete(wa.appStateFullSyncAttempted, evt.Name)
|
||||
}
|
||||
delete(meta.AppStateRecoveryAttempted, evt.Name)
|
||||
err := wa.UserLogin.Save(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -969,7 +1018,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev
|
|||
return
|
||||
}
|
||||
if !evt.FullSync {
|
||||
if !lastFullSync.IsZero() {
|
||||
if !lastFullSync.IsZero() && time.Since(lastFullSync) < 48*time.Hour {
|
||||
log.Debug().
|
||||
Err(evt.Error).
|
||||
Time("last_full_sync_attempt", lastFullSync).
|
||||
|
|
@ -1016,3 +1065,110 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev
|
|||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) resyncBlocklist(ctx context.Context) func() bool {
|
||||
wa.blocklistLock.Lock()
|
||||
defer wa.blocklistLock.Unlock()
|
||||
return wa.resyncBlocklistUnlocked(ctx)
|
||||
}
|
||||
|
||||
func noopUpdates() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) resyncBlocklistUnlocked(ctx context.Context) func() bool {
|
||||
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
|
||||
dhash := ptr.Val(meta.Blocklist).DHash
|
||||
newBlocklist, err := wa.Client.GetBlocklist(ctx, dhash)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get new blocklist")
|
||||
return noopUpdates
|
||||
} else if newBlocklist == nil {
|
||||
zerolog.Ctx(ctx).Debug().Msg("Blocklist didn't change")
|
||||
return noopUpdates
|
||||
}
|
||||
updateEvents := wa.saveBlocklistUnlocked(ctx, newBlocklist)
|
||||
return func() bool {
|
||||
for _, evt := range updateEvents {
|
||||
res := wa.UserLogin.QueueRemoteEvent(evt)
|
||||
if !res.Success {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) saveBlocklistUnlocked(ctx context.Context, newBlocklist *types.Blocklist) []*simplevent.ChatInfoChange {
|
||||
if newBlocklist.AddressingMode != types.AddressingModeLID {
|
||||
zerolog.Ctx(ctx).Warn().Msg("Non-LID addressing mode in blocklist")
|
||||
}
|
||||
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
|
||||
oldBlocklist := ptr.Val(meta.Blocklist)
|
||||
currentlyBlockedDMs, err := wa.Main.Bridge.DB.Portal.GetAllBlockedDMsOf(ctx, wa.UserLogin.ID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get currently blocked DMs")
|
||||
}
|
||||
// In addition to diffing the cached blocklist, fetch all blocked DM portals from the database
|
||||
// to ensure any missed updates are retried.
|
||||
oldBlockedLIDs := exslices.CastFuncFilter[types.JID](currentlyBlockedDMs, func(key networkid.PortalKey) (types.JID, bool) {
|
||||
jid, _ := waid.ParsePortalID(key.ID)
|
||||
return jid, !jid.IsEmpty()
|
||||
})
|
||||
removed, added := exslices.Diff(append(oldBlocklist.LIDs(), oldBlockedLIDs...), newBlocklist.LIDs())
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Int("old_count", len(oldBlocklist.Items)).
|
||||
Int("db_count", len(currentlyBlockedDMs)).
|
||||
Int("new_count", len(newBlocklist.Items)).
|
||||
Any("removed", removed).
|
||||
Any("added", added).
|
||||
Msg("Syncing blocklist")
|
||||
var updateEvents []*simplevent.ChatInfoChange
|
||||
for _, jid := range removed {
|
||||
if evt := wa.makeBlockUpdateEvent(ctx, jid, false); evt != nil {
|
||||
updateEvents = append(updateEvents, evt)
|
||||
}
|
||||
}
|
||||
for _, jid := range added {
|
||||
if evt := wa.makeBlockUpdateEvent(ctx, jid, true); evt != nil {
|
||||
updateEvents = append(updateEvents, evt)
|
||||
}
|
||||
}
|
||||
meta.Blocklist = newBlocklist
|
||||
err = wa.UserLogin.Save(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save blocklist to login metadata")
|
||||
}
|
||||
return updateEvents
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) makeBlockUpdateEvent(ctx context.Context, jid types.JID, blocked bool) *simplevent.ChatInfoChange {
|
||||
if jid.Server == types.DefaultUserServer {
|
||||
var err error
|
||||
jid, err = wa.GetStore().LIDs.GetLIDForPN(ctx, jid)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("pn", jid).Msg("Failed to get LID for blocklist update")
|
||||
return nil
|
||||
} else if jid.IsEmpty() {
|
||||
zerolog.Ctx(ctx).Debug().Stringer("pn", jid).Msg("No LID for blocklist update")
|
||||
return nil
|
||||
}
|
||||
} else if jid.Server != types.HiddenUserServer && jid.Server != types.BotServer {
|
||||
zerolog.Ctx(ctx).Debug().Stringer("jid", jid).Msg("Unexpected JID server in blocklist")
|
||||
return nil
|
||||
}
|
||||
return &simplevent.ChatInfoChange{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatInfoChange,
|
||||
PortalKey: wa.makeWAPortalKey(jid),
|
||||
LogContext: func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str("wa_event_type", "blocklist update")
|
||||
},
|
||||
},
|
||||
ChatInfoChange: &bridgev2.ChatInfoChange{
|
||||
ChatInfo: &bridgev2.ChatInfo{
|
||||
UserBlocked: &blocked,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,3 +82,18 @@ func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, jid types.JI
|
|||
}
|
||||
return jid
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) getRealSender(chat, sender types.JID, fromMe bool) types.JID {
|
||||
if sender.IsEmpty() {
|
||||
if fromMe {
|
||||
if chat.Server == types.HiddenUserServer {
|
||||
sender = wa.GetLID().ToNonAD()
|
||||
} else {
|
||||
sender = wa.JID.ToNonAD()
|
||||
}
|
||||
} else if chat.Server == types.DefaultUserServer || chat.Server == types.HiddenUserServer || chat.Server == types.BotServer {
|
||||
sender = chat.ToNonAD()
|
||||
}
|
||||
}
|
||||
return sender
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package connector
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
|
@ -25,9 +26,11 @@ import (
|
|||
"go.mau.fi/util/dbutil"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
|
@ -47,6 +50,8 @@ func (wa *WhatsAppClient) FindAltTargetMessage(ctx context.Context, targetMsg ne
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if parsed.Chat == types.NewMetaAIJID {
|
||||
altChat = types.MetaAIJID
|
||||
}
|
||||
if !altSender.IsEmpty() {
|
||||
altSenderID := *parsed
|
||||
|
|
@ -58,6 +63,8 @@ func (wa *WhatsAppClient) FindAltTargetMessage(ctx context.Context, targetMsg ne
|
|||
altChatID.Chat = altChat
|
||||
if altSender.Server == types.DefaultUserServer {
|
||||
altChatID.Sender = altSender
|
||||
} else if parsed.Sender == types.NewMetaAIJID {
|
||||
altChatID.Sender = types.MetaAIJID
|
||||
}
|
||||
alts = append(alts, altChatID.String())
|
||||
}
|
||||
|
|
@ -211,6 +218,30 @@ func (wa *WhatsAppConnector) migrateToLIDDMs(ctx context.Context) error {
|
|||
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 {
|
||||
if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_avatars_resynced") == "true" {
|
||||
return nil
|
||||
|
|
@ -267,3 +298,84 @@ func (wa *WhatsAppConnector) syncMismatchingGhosts(ctx context.Context) error {
|
|||
wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_avatars_resynced", "true")
|
||||
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
|
||||
})
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue