Watch
1
0
Fork
You've already forked mautrix-whatsapp
0
mirror of https://github.com/mautrix/whatsapp.git synced 2026-09-16 07:42:07 -04:00

Compare commits

...
Author SHA1 Message Date
Tulir Asokan
52e75964a0 Bump version to v26.09 2026-09-16 13:15:31 +03:00
Toni Spets
9361103b64
backfill: don't panic when the whatsmeow client is nil (#954) 2026-09-16 12:00:30 +03:00
Tulir Asokan
741f45d649 dependencies: update whatsmeow 2026-09-16 00:22:26 +03:00
Tulir Asokan
1ea9f02ade msgconv/from-whatsapp: bridge advanced rich responses as unknown message types 2026-09-14 19:28:43 +03:00
Tulir Asokan
a76341f281 handlewhatsapp: log unknown protocol message types 2026-09-14 18:18:37 +03:00
Tulir Asokan
c1e30935dc dependencies: update whatsmeow again 2026-09-14 18:07:29 +03:00
Tulir Asokan
991c12cdab changelog: update 2026-09-14 15:35:08 +03:00
Tulir Asokan
037423f0b2 dependencies: update whatsmeow 2026-09-14 15:34:54 +03:00
Tulir Asokan
d6bfdeb477 dependencies: update mautrix-go 2026-09-11 21:33:53 +03:00
Tulir Asokan
5d3fce8eff handlewhatsapp: ignore sent broadcasts 2026-09-11 19:42:24 +03:00
Tulir Asokan
18ae384c98 .github: make contributing.md more visible 2026-09-11 19:03:14 +03:00
Tulir Asokan
79164b22ad lidmigrate: resync ghost avatars in background 2026-09-10 10:42:06 +03:00
Tulir Asokan
9ea41dcb0e login: add support for adv rotation 2026-09-09 12:40:38 +03:00
Tulir Asokan
82c79d1108 dependencies: update mautrix-go 2026-09-08 19:09:47 +03:00
Tulir Asokan
a19ef78dd3 directmedia: don't allow parallel downloads of the same avatar 2026-09-08 15:03:18 +03:00
Tulir Asokan
446bd22695 lidmigrate: resync missing avatars and reuse info for alt ghost sync 2026-09-08 14:42:31 +03:00
Tulir Asokan
63dd2582b5 dependencies: update 2026-09-08 14:41:40 +03:00
Tulir Asokan
240648081e startchat: return user info with resolve identifier requests 2026-09-03 14:37:14 +03:00
Tulir Asokan
6302a1c709 userinfo: add option to lazily fetch avatars using direct media 2026-09-03 14:29:18 +03:00
Tulir Asokan
51504db309 chatinfo: add option to limit eager member sync in groups 2026-09-03 14:17:26 +03:00
Tulir Asokan
d1dbe393b6 dependencies: update whatsmeow 2026-09-03 14:17:26 +03:00
Tulir Asokan
a7bf2c89f8 dependencies: update 2026-08-29 01:57:40 +03:00
Tulir Asokan
e59633c991 handlewhatsapp: fix handling newsletter read receipts 2026-08-25 23:59:48 +03:00
Tulir Asokan
4973412350 dependencies: update mautrix-go 2026-08-25 17:36:16 +03:00
Nick Mills-Barrett
508b35e1ae
directmedia: mark media retry decrypt failures (#939)
Co-authored-by: Nick Mills-Barrett <nick@Nicks-Mac-Studio.local>
2026-08-25 12:33:30 +01:00
Kishan Bagaria
c4aabf0b1a
handlewhatsapp: fix nested HD media caption edits (#948) 2026-08-24 06:51:06 -07:00
Tulir Asokan
6f767fb951 dependencies: fix go mod tidy 2026-08-24 16:46:43 +03:00
Nick Mills-Barrett
39bc900ac9
dependencies: update mautrix-go 2026-08-24 11:33:43 +01:00
Tulir Asokan
e1220a8d4e handlewhatsapp: delete history sync data on logout 2026-08-24 13:30:35 +03:00
Tulir Asokan
03eb390082 backfill: return no more messages flag if there's no anchor message 2026-08-24 13:28:23 +03:00
Tulir Asokan
d70eb6b135 dependencies: update whatsmeow 2026-08-21 17:25:12 +03:00
Tulir Asokan
bbc9a37b30 dependencies: bump minimum Go version to 1.26 2026-08-20 18:37:07 +03:00
Tulir Asokan
ba58807b0f
backfill: clamp message timestamps in history syncs (#946) 2026-08-20 16:09:02 +03:00
Tulir Asokan
5ef4c47b44 chatinfo: always allow poll responses in power levels 2026-08-19 18:53:20 +03:00
Nick Mills-Barrett
0f1832595f
login: split connect and phone pair timeouts, increase to 30s (#947) 2026-08-19 15:41:15 +01:00
23 changed files with 429 additions and 154 deletions

View file

@ -11,14 +11,14 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
go-version: ["1.25", "1.26"] go-version: ["1.26", "1.27"]
name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }} name: Lint ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v7
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
cache: true cache: true

View file

@ -1,3 +1,21 @@
# v26.09
* Bumped minimum Go version to 1.26.
* Added option to limit eager member sync in group chat portals.
* Added option to fetch avatars lazily when using direct media.
* Added one-time migration to ensure LID ghost avatars are consistent with the
old phone number ghosts.
* Fixed the resolve identifier command and API not returning any user info for
users who hadn't previously been encountered.
* Fixed group portal power levels to always allow poll responses.
* Fixed handling history sync events where the phone sends nonsensical
timestamps.
* Fixed handling edits to HD media captions.
* Fixed handling bridging own read receipts from the native apps in channels.
* Fixed decrypting messages from the new Muse AI bot
* May require resyncing app state using `!wa sync appstate regular_high`.
* The message contents aren't supported yet.
# v26.08 # v26.08
* Switched direct chats to use LIDs instead of phone numbers. * Switched direct chats to use LIDs instead of phone numbers.

1
CONTRIBUTING.md Normal file
View file

@ -0,0 +1 @@
See <https://docs.mau.fi/bridges/general/contributing.html>

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.08", Version: "26.09",
SemCalVer: true, SemCalVer: true,
Connector: &connector.WhatsAppConnector{}, Connector: &connector.WhatsAppConnector{},
} }

32
go.mod
View file

@ -1,8 +1,8 @@
module go.mau.fi/mautrix-whatsapp module go.mau.fi/mautrix-whatsapp
go 1.25.0 go 1.26.0
toolchain go1.26.6 toolchain go1.27.1
tool go.mau.fi/util/cmd/maubuild tool go.mau.fi/util/cmd/maubuild
@ -10,15 +10,15 @@ require (
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.19.0 github.com/tidwall/gjson v1.19.0
go.mau.fi/util v0.10.0 go.mau.fi/util v0.10.1
go.mau.fi/webp v0.3.0 go.mau.fi/webp v0.3.0
go.mau.fi/whatsmeow v0.0.0-20260816113502-fb386f152837 go.mau.fi/whatsmeow v0.0.0-20260916100317-2375e1751bbd
golang.org/x/image v0.45.0 golang.org/x/image v0.46.0
golang.org/x/net v0.58.0 golang.org/x/net v0.59.0
golang.org/x/sync v0.22.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.30.0 maunium.net/go/mautrix v0.31.0
) )
require ( require (
@ -31,8 +31,8 @@ require (
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
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.49 // indirect github.com/mattn/go-sqlite3 v1.14.52 // indirect
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b // 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
@ -40,14 +40,14 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/vektah/gqlparser/v2 v2.5.27 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect
github.com/yuin/goldmark v1.8.5 // indirect github.com/yuin/goldmark v1.8.6 // 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.55.0 // indirect golang.org/x/crypto v0.57.0 // indirect
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect
golang.org/x/mod v0.40.0 // indirect golang.org/x/mod v0.41.0 // indirect
golang.org/x/sys v0.47.0 // indirect golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.41.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

64
go.sum
View file

@ -13,8 +13,6 @@ github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6p
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@ -34,13 +32,11 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
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.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w= github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b h1:sS7HLzwS+dO+gxATgQfeZDEdUZe2pKAB3nGoUwP5zU0= github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 h1:lcWAnrqr2nNfDiArwFNHCE4787Mw2tCdVSOXCru0/0E=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
@ -52,8 +48,8 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= 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.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
@ -67,35 +63,35 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
github.com/yuin/goldmark v1.8.5/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.10.0 h1:vH9IXZmfBKa96p47HxrVqEPkrj02zDJg3o4EF172+Lk= go.mau.fi/util v0.10.1 h1:1oSqb4TwzLA0cUDY0aomyBPFKkZ3J5fqCmrXV3VH3GQ=
go.mau.fi/util v0.10.0/go.mod h1:uZwpm9sK4wO2Qqy+t6QoVq29szMsRxWXp9/BkQLG4xk= go.mau.fi/util v0.10.1/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-20260816113502-fb386f152837 h1:xJ13dqFcK/oPNImltlk7sumI/uiQ9EON/6g/iwpT1Nc= go.mau.fi/whatsmeow v0.0.0-20260916100317-2375e1751bbd h1:ZRMG9rK+Vghe4U+xGDGI2hkwJgoPTqf25HWm3nkBM8w=
go.mau.fi/whatsmeow v0.0.0-20260816113502-fb386f152837/go.mod h1:UFP0D7aj+biuFejsllcoNiHTQorh0ODFUyG4Txpqfks= go.mau.fi/whatsmeow v0.0.0-20260916100317-2375e1751bbd/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.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY= golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk= golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0= golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4= golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= 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=
@ -107,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.30.0 h1:bad+q7w5tLqiHpr+oUxVI+8m8ePbV3AvoFKg2jQzPyo= maunium.net/go/mautrix v0.31.0 h1:x6XNBSa0kOaBgcleI+ZBLkGvBdHIE0qdeY6TZ4wWUeI=
maunium.net/go/mautrix v0.30.0/go.mod h1:bb0gjxbTFOqTaAYKGw5E7j9XROUR2Sl1Etm3IbmYXbo= maunium.net/go/mautrix v0.31.0/go.mod h1:vfPYtoGAjlTnIF+W8tl+rjX3yB4PEfqYR2sDXvQo4Co=

View file

@ -247,12 +247,14 @@ func (wa *WhatsAppClient) handleWAHistorySync(
return c.Stringer("chat_jid", jid) return c.Stringer("chat_jid", jid)
}) })
var minTime, maxTime, firstItemTime, lastItemTime time.Time var firstItemTime, lastItemTime time.Time
var minTimeIndex, maxTimeIndex int
ignoredTypes := 0 ignoredTypes := 0
messages := make([]*wadb.HistorySyncMessageTuple, 0, len(conv.GetMessages())) rawMessages := conv.GetMessages()
for i, rawMsg := range conv.GetMessages() { messages := make([]*wadb.HistorySyncMessageTuple, 0, len(rawMessages))
allowClamp := conv.GetCommentsCount() == 0
var newerTS uint64
for i, rawMsg := range rawMessages {
// Don't store messages that will just be skipped. // Don't store messages that will just be skipped.
msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage()) msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage())
if err != nil { if err != nil {
@ -270,20 +272,30 @@ func (wa *WhatsAppClient) handleWAHistorySync(
firstItemTime = msgEvt.Info.Timestamp firstItemTime = msgEvt.Info.Timestamp
} }
lastItemTime = msgEvt.Info.Timestamp lastItemTime = msgEvt.Info.Timestamp
if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) {
minTime = msgEvt.Info.Timestamp
minTimeIndex = i
}
if maxTime.IsZero() || msgEvt.Info.Timestamp.After(maxTime) {
maxTime = msgEvt.Info.Timestamp
maxTimeIndex = i
}
msgType := getMessageType(msgEvt.Message) msgType := getMessageType(msgEvt.Message)
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") { if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
ignoredTypes++ ignoredTypes++
continue continue
} }
// Comments (replies) in announcement groups are not ordered by timestamp, so don't clamp them.
if rawMsg.GetMessage().GetCommentMetadata().GetCommentParentKey() != nil {
allowClamp = false
}
// WhatsApp has bugs where some random messages will have timestamps decades in the future.
// To ensure they don't mess up our ordering, require timestamps of older messages to be
// before the previous (newer) message.
if currentTS := rawMsg.GetMessage().GetMessageTimestamp(); newerTS > 0 && allowClamp && currentTS > newerTS {
log.Warn().
Time("current_ts", time.Unix(int64(currentTS), 0)).
Time("prev_ts", time.Unix(int64(newerTS), 0)).
Int("msg_index", i).
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
Msg("Clamping message timestamp")
rawMsg.Message.MessageTimestamp = ptr.Ptr(newerTS)
msgEvt.Info.Timestamp = time.Unix(int64(newerTS), 0)
}
newerTS = rawMsg.GetMessage().GetMessageTimestamp()
marshaled, err := proto.Marshal(rawMsg) marshaled, err := proto.Marshal(rawMsg)
if err != nil { if err != nil {
log.Warn().Err(err). log.Warn().Err(err).
@ -298,13 +310,8 @@ func (wa *WhatsAppClient) handleWAHistorySync(
log.Debug(). log.Debug().
Int("wrapped_count", len(messages)). Int("wrapped_count", len(messages)).
Int("ignored_msg_type_count", ignoredTypes). Int("ignored_msg_type_count", ignoredTypes).
Time("lowest_time", minTime).
Int("lowest_time_index", minTimeIndex).
Time("highest_time", maxTime).
Int("highest_time_index", maxTimeIndex).
Time("first_item_time", firstItemTime). Time("first_item_time", firstItemTime).
Time("last_item_time", lastItemTime). Time("last_item_time", lastItemTime).
Bool("highest_time_mismatch", firstItemTime != maxTime).
Dict("metadata", zerolog.Dict(). Dict("metadata", zerolog.Dict().
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()). Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()). Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
@ -320,7 +327,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Msg("Collected messages to save from history sync conversation") Msg("Collected messages to save from history sync conversation")
if len(messages) > 0 { if len(messages) > 0 {
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime)) err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, firstItemTime))
if err != nil { if err != nil {
if stopOnError { if stopOnError {
return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err) return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
@ -525,6 +532,7 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
return nil, fmt.Errorf("failed to load messages from database: %w", err) return nil, fmt.Errorf("failed to load messages from database: %w", err)
} else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) { } else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) {
wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0) wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0)
hasMore = hasMore && params.AnchorMessage != nil
if hasMore && !params.AllowSlowFetch { if hasMore && !params.AllowSlowFetch {
return &bridgev2.FetchMessagesResponse{ return &bridgev2.FetchMessagesResponse{
MoreRequiresSlowFetch: true, MoreRequiresSlowFetch: true,
@ -594,6 +602,9 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
messages []*waWeb.WebMessageInfo, messages []*waWeb.WebMessageInfo,
explodeOnError bool, explodeOnError bool,
) (*bridgev2.FetchMessagesResponse, error) { ) (*bridgev2.FetchMessagesResponse, error) {
if wa.Client == nil {
return nil, bridgev2.ErrNotLoggedIn
}
oldestTS := messages[len(messages)-1].GetMessageTimestamp() oldestTS := messages[len(messages)-1].GetMessageTimestamp()
newestTS := messages[0].GetMessageTimestamp() newestTS := messages[0].GetMessageTimestamp()
convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages)) convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages))

View file

@ -29,6 +29,8 @@ func (wa *WhatsAppClient) GetChatInfo(ctx context.Context, portal *bridgev2.Port
return wa.getChatInfo(ctx, portalJID, nil, portal.MXID == "") return wa.getChatInfo(ctx, portalJID, nil, portal.MXID == "")
} }
var ErrBroadcastList = errors.New("broadcast list bridging is currently not supported")
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 {
case types.DefaultUserServer, types.HiddenUserServer, types.BotServer: case types.DefaultUserServer, types.HiddenUserServer, types.BotServer:
@ -37,7 +39,7 @@ func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID,
if portalJID == types.StatusBroadcastJID { if portalJID == types.StatusBroadcastJID {
wrapped = wa.wrapStatusBroadcastInfo(ctx) wrapped = wa.wrapStatusBroadcastInfo(ctx)
} else { } else {
return nil, fmt.Errorf("broadcast list bridging is currently not supported") return nil, ErrBroadcastList
} }
case types.GroupServer: case types.GroupServer:
info, err := wa.Client.GetGroupInfo(ctx, portalJID) info, err := wa.Client.GetGroupInfo(ctx, portalJID)
@ -271,11 +273,12 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
setAddressingMode(info.AddressingMode), setAddressingMode(info.AddressingMode),
setTopicID(info.TopicID, info.Topic), setTopicID(info.TopicID, info.Topic),
) )
syncAllMembers := wa.Main.Config.MaxMemberSync < 0 || len(info.Participants) < wa.Main.Config.MaxMemberSync
wrapped := &bridgev2.ChatInfo{ wrapped := &bridgev2.ChatInfo{
Name: ptr.Ptr(info.Name), Name: ptr.Ptr(info.Name),
Topic: ptr.Ptr(info.Topic), Topic: ptr.Ptr(info.Topic),
Members: &bridgev2.ChatMemberList{ Members: &bridgev2.ChatMemberList{
IsFull: !info.IsIncognito && !info.IsParent, IsFull: !info.IsIncognito && !info.IsParent && syncAllMembers,
TotalMemberCount: len(info.Participants), TotalMemberCount: len(info.Participants),
MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)), MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)),
PowerLevels: &bridgev2.PowerLevelOverrides{ PowerLevels: &bridgev2.PowerLevelOverrides{
@ -284,14 +287,13 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
Ban: ptr.Ptr(nobodyPL), Ban: ptr.Ptr(nobodyPL),
// TODO allow invites if bridge config says to allow them, or maybe if relay mode is enabled? // TODO allow invites if bridge config says to allow them, or maybe if relay mode is enabled?
Events: map[event.Type]int{ Events: map[event.Type]int{
event.StateRoomName: metaChangePL, event.StateRoomName: metaChangePL,
event.StateRoomAvatar: metaChangePL, event.StateRoomAvatar: metaChangePL,
event.StateTopic: metaChangePL, event.StateTopic: metaChangePL,
event.EventReaction: defaultPL, event.EventReaction: defaultPL,
event.EventRedaction: defaultPL, event.EventRedaction: defaultPL,
event.EventUnstablePollResponse: defaultPL,
event.StateBeeperDisappearingTimer: metaChangePL, event.StateBeeperDisappearingTimer: metaChangePL,
// TODO always allow poll responses
}, },
}, },
}, },
@ -316,6 +318,9 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
} else if pcp.IsAdmin { } else if pcp.IsAdmin {
member.PowerLevel = ptr.Ptr(adminPL) member.PowerLevel = ptr.Ptr(adminPL)
} else { } else {
if !syncAllMembers && !member.EventSender.IsFromMe {
continue
}
member.PowerLevel = ptr.Ptr(defaultPL) member.PowerLevel = ptr.Ptr(defaultPL)
} }
member.MemberEventExtra = map[string]any{ member.MemberEventExtra = map[string]any{
@ -551,12 +556,12 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
StateDefault: ptr.Ptr(nobodyPL), StateDefault: ptr.Ptr(nobodyPL),
Ban: ptr.Ptr(nobodyPL), Ban: ptr.Ptr(nobodyPL),
Events: map[event.Type]int{ Events: map[event.Type]int{
event.StateRoomName: adminPL, event.StateRoomName: adminPL,
event.StateRoomAvatar: adminPL, event.StateRoomAvatar: adminPL,
event.StateTopic: adminPL, event.StateTopic: adminPL,
event.EventReaction: defaultPL, event.EventReaction: defaultPL,
event.EventRedaction: defaultPL, event.EventRedaction: defaultPL,
// TODO always allow poll responses event.EventUnstablePollResponse: defaultPL,
}, },
}, },
}, },

View file

@ -78,7 +78,7 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
w.Client = whatsmeow.NewClient(w.Device, waLog.Zerolog(log)) w.Client = whatsmeow.NewClient(w.Device, waLog.Zerolog(log))
w.Client.AddEventHandlerWithSuccessStatus(w.handleWAEvent) w.Client.AddEventHandlerWithSuccessStatus(w.handleWAEvent)
w.Client.SynchronousAck = true w.Client.SynchronousAck = true
w.Client.EnableDecryptedEventBuffer = bridgev2.PortalEventBuffer == 0 w.Client.EnableDecryptedEventBuffer = wa.Bridge.Config.PortalEventBuffer == 0
w.Client.ManualHistorySyncDownload = true w.Client.ManualHistorySyncDownload = true
w.Client.SendReportingTokens = true w.Client.SendReportingTokens = true
w.Client.AutomaticMessageRerequestFromPhone = true w.Client.AutomaticMessageRerequestFromPhone = true
@ -116,6 +116,7 @@ type WhatsAppClient struct {
nextResync time.Time nextResync time.Time
directMediaRetries map[networkid.MessageID]*directMediaRetry directMediaRetries map[networkid.MessageID]*directMediaRetry
directMediaLock sync.Mutex directMediaLock sync.Mutex
avatarLock exsync.KeyedMutex[types.JID]
mediaRetryLock *semaphore.Weighted mediaRetryLock *semaphore.Weighted
offlineSyncWaiter atomic.Pointer[chan error] offlineSyncWaiter atomic.Pointer[chan error]
isNewLogin bool isNewLogin bool
@ -393,6 +394,10 @@ func (wa *WhatsAppClient) LogoutRemote(ctx context.Context) {
} }
wa.Disconnect() wa.Disconnect()
wa.Client = nil wa.Client = nil
err := wa.Main.DB.Conversation.DeleteAll(ctx, wa.UserLogin.ID)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete history sync data on logout")
}
} }
func (wa *WhatsAppClient) IsLoggedIn() bool { func (wa *WhatsAppClient) IsLoggedIn() bool {

View file

@ -52,6 +52,8 @@ type Config struct {
DirectMediaAutoRequest bool `yaml:"direct_media_auto_request"` DirectMediaAutoRequest bool `yaml:"direct_media_auto_request"`
InitialAutoReconnect bool `yaml:"initial_auto_reconnect"` InitialAutoReconnect bool `yaml:"initial_auto_reconnect"`
UseWhatsAppRetryStore bool `yaml:"use_whatsapp_retry_store"` UseWhatsAppRetryStore bool `yaml:"use_whatsapp_retry_store"`
MaxMemberSync int `yaml:"max_member_sync"`
LazyAvatars bool `yaml:"lazy_avatars"`
AnimatedSticker msgconv.AnimatedStickerConfig `yaml:"animated_sticker"` AnimatedSticker msgconv.AnimatedStickerConfig `yaml:"animated_sticker"`
@ -129,6 +131,8 @@ func upgradeConfig(helper up.Helper) {
helper.Copy(up.Bool, "direct_media_auto_request") helper.Copy(up.Bool, "direct_media_auto_request")
helper.Copy(up.Bool, "initial_auto_reconnect") helper.Copy(up.Bool, "initial_auto_reconnect")
helper.Copy(up.Bool, "use_whatsapp_retry_store") helper.Copy(up.Bool, "use_whatsapp_retry_store")
helper.Copy(up.Int, "max_member_sync")
helper.Copy(up.Bool, "lazy_avatars")
helper.Copy(up.Str, "animated_sticker", "target") helper.Copy(up.Str, "animated_sticker", "target")
helper.Copy(up.Int, "animated_sticker", "args", "width") helper.Copy(up.Int, "animated_sticker", "args", "width")
@ -205,6 +209,7 @@ func (wa *WhatsAppConnector) GetConfig() (string, any, up.Upgrader) {
{"proxy"}, {"proxy"},
{"displayname_template"}, {"displayname_template"},
{"call_start_notices"}, {"call_start_notices"},
{"animated_sticker"},
{"history_sync"}, {"history_sync"},
}, },
Base: ExampleConfig, Base: ExampleConfig,

View file

@ -142,6 +142,9 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) {
} }
func (wa *WhatsAppConnector) Start(ctx context.Context) error { func (wa *WhatsAppConnector) Start(ctx context.Context) error {
if !wa.MsgConv.DirectMedia && wa.Config.LazyAvatars {
return fmt.Errorf("lazy_avatars set without enabling global direct_media")
}
err := wa.DeviceStore.Upgrade(ctx) err := wa.DeviceStore.Upgrade(ctx)
if err != nil { if err != nil {
return bridgev2.DBUpgradeError{Err: err, Section: "whatsmeow"} return bridgev2.DBUpgradeError{Err: err, Section: "whatsmeow"}
@ -157,7 +160,17 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error {
return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"} return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"}
} }
return wa.migrateToLIDDMs(ctx) err = wa.migrateToLIDDMs(ctx)
if err != nil {
return fmt.Errorf("failed to migrate to LID DMs: %w", err)
}
go func() {
err = wa.syncMismatchingGhosts(wa.Bridge.BackgroundCtx)
if err != nil {
wa.Bridge.Log.Err(err).Msg("Failed to sync mismatching ghosts")
}
}()
return nil
} }
func (wa *WhatsAppConnector) Stop() { func (wa *WhatsAppConnector) Stop() {

View file

@ -30,6 +30,7 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exsync" "go.mau.fi/util/exsync"
"go.mau.fi/util/jsontime"
"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"
@ -84,13 +85,15 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars
if waClient.Client == nil { if waClient.Client == nil {
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)
} }
waClient.avatarLock.Lock(parsedID.Avatar.TargetJID)
defer waClient.avatarLock.Unlock(parsedID.Avatar.TargetJID)
cachedInfo, err := wa.DB.AvatarCache.Get(ctx, parsedID.Avatar.TargetJID, parsedID.Avatar.AvatarID) cachedInfo, err := wa.DB.AvatarCache.Get(ctx, parsedID.Avatar.TargetJID, parsedID.Avatar.AvatarID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get avatar cache entry: %w", err) return nil, fmt.Errorf("failed to get avatar cache entry: %w", err)
} }
if cachedInfo != nil && cachedInfo.Gone { if cachedInfo.IsGone() {
return nil, mautrix.MNotFound.WithMessage("Avatar is no longer available (cached response)") return nil, mautrix.MNotFound.WithMessage("Avatar is no longer available (cached response)")
} else if cachedInfo == nil || cachedInfo.Expiry.Time.Before(time.Now().Add(5*time.Minute)) { } else if cachedInfo.Expired() {
zerolog.Ctx(ctx).Debug(). zerolog.Ctx(ctx).Debug().
Str("avatar_id", parsedID.Avatar.AvatarID). Str("avatar_id", parsedID.Avatar.AvatarID).
Msg("Refreshing avatar URL from WhatsApp servers") Msg("Refreshing avatar URL from WhatsApp servers")
@ -99,7 +102,7 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars
}) })
if errors.Is(err, whatsmeow.ErrProfilePictureNotSet) || if errors.Is(err, whatsmeow.ErrProfilePictureNotSet) ||
errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) || errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) ||
(err == nil && (avatar == nil || avatar.ID != parsedID.Avatar.AvatarID)) { (err == nil && (avatar == nil || (avatar.ID != parsedID.Avatar.AvatarID && !parsedID.Avatar.IsRandom()))) {
zerolog.Ctx(ctx).Debug(). zerolog.Ctx(ctx).Debug().
Err(err). Err(err).
Stringer("target_jid", parsedID.Avatar.TargetJID). Stringer("target_jid", parsedID.Avatar.TargetJID).
@ -107,9 +110,14 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars
Str("wanted_avatar_id", parsedID.Avatar.AvatarID). Str("wanted_avatar_id", parsedID.Avatar.AvatarID).
Str("got_avatar_id", ptr.Val(avatar).ID). Str("got_avatar_id", ptr.Val(avatar).ID).
Msg("Avatar is no longer available") Msg("Avatar is no longer available")
var goneExpiry jsontime.Unix
if parsedID.Avatar.IsRandom() {
goneExpiry = jsontime.U(time.Now().Add(7 * 24 * time.Hour))
}
err = wa.DB.AvatarCache.Put(ctx, &wadb.AvatarCacheEntry{ err = wa.DB.AvatarCache.Put(ctx, &wadb.AvatarCacheEntry{
EntityJID: parsedID.Avatar.TargetJID, EntityJID: parsedID.Avatar.TargetJID,
AvatarID: parsedID.Avatar.AvatarID, AvatarID: parsedID.Avatar.AvatarID,
Expiry: goneExpiry,
Gone: true, Gone: true,
}) })
if err != nil { if err != nil {
@ -128,6 +136,15 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars
Str("avatar_id", avatar.ID). Str("avatar_id", avatar.ID).
Msg("Failed to update avatar cache entry") Msg("Failed to update avatar cache entry")
} }
if cachedInfo.AvatarID != parsedID.Avatar.AvatarID {
cachedInfo.AvatarID = parsedID.Avatar.AvatarID
err = wa.DB.AvatarCache.Put(ctx, cachedInfo)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).
Str("avatar_id", parsedID.Avatar.AvatarID).
Msg("Failed to update avatar cache entry")
}
}
} }
return &mediaproxy.GetMediaResponseFile{ return &mediaproxy.GetMediaResponseFile{
Callback: func(w *os.File) (*mediaproxy.FileMeta, error) { Callback: func(w *os.File) (*mediaproxy.FileMeta, error) {
@ -255,10 +272,11 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse(
type directMediaRetry struct { type directMediaRetry struct {
sync.Mutex sync.Mutex
resultURL string resultURL string
wait *exsync.Event wait *exsync.Event
requested bool requested bool
resultType waMmsRetry.MediaRetryNotification_ResultType resultType waMmsRetry.MediaRetryNotification_ResultType
decryptFail bool
} }
func (wa *WhatsAppClient) getDirectMediaRetryState(msgID networkid.MessageID, create bool) *directMediaRetry { func (wa *WhatsAppClient) getDirectMediaRetryState(msgID networkid.MessageID, create bool) *directMediaRetry {
@ -285,6 +303,9 @@ func (wa *WhatsAppClient) requestAndWaitDirectMedia(ctx context.Context, rawMsgI
keys.DirectPath = state.resultURL keys.DirectPath = state.resultURL
return nil return nil
} }
if state.decryptFail {
return mautrix.MNotFound.WithMessage("Unable to retrieve media: failed to decrypt the media retry notification from your phone.")
}
switch state.resultType { switch state.resultType {
case waMmsRetry.MediaRetryNotification_NOT_FOUND: case waMmsRetry.MediaRetryNotification_NOT_FOUND:
return mautrix.MNotFound.WithMessage("This media was not found on your phone.") return mautrix.MNotFound.WithMessage("This media was not found on your phone.")
@ -338,6 +359,9 @@ func (wa *WhatsAppClient) receiveDirectMediaRetry(ctx context.Context, msg *data
retryData, err := whatsmeow.DecryptMediaRetryNotification(retry, keys.Key) retryData, err := whatsmeow.DecryptMediaRetryNotification(retry, keys.Key)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("Failed to decrypt media retry notification") log.Warn().Err(err).Msg("Failed to decrypt media retry notification")
if state != nil {
state.decryptFail = true
}
return return
} }
if state != nil { if state != nil {

View file

@ -69,6 +69,12 @@ initial_auto_reconnect: true
# the bridge? By default, the bridge only stores messages in memory, and therefore can't accept # the bridge? By default, the bridge only stores messages in memory, and therefore can't accept
# retry receipts if the bridge is restarted after the message is sent. # retry receipts if the bridge is restarted after the message is sent.
use_whatsapp_retry_store: false use_whatsapp_retry_store: false
# Maximum size of groups to sync members eagerly in. Defaults to unlimited.
# If set, groups with more than this number of members will not have non-admin participants synced until they talk.
max_member_sync: -1
# Lazily load avatars using direct media?
# Global direct media must be enabled for this.
lazy_avatars: false
# Settings for converting animated stickers. # Settings for converting animated stickers.
animated_sticker: animated_sticker:

View file

@ -24,9 +24,11 @@ import (
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exslices"
"go.mau.fi/util/ptr" "go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/appstate" "go.mau.fi/whatsmeow/appstate"
"go.mau.fi/whatsmeow/proto/waCommon"
"go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events" "go.mau.fi/whatsmeow/types/events"
@ -202,7 +204,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
wa.notifyOfflineSyncWaiter(nil) wa.notifyOfflineSyncWaiter(nil)
case *events.LoggedOut: case *events.LoggedOut:
wa.handleWALogout(evt.Reason, evt.OnConnect) wa.handleWALogout(ctx, evt.Reason, evt.OnConnect)
wa.notifyOfflineSyncWaiter(fmt.Errorf("logged out: %s", evt.Reason)) wa.notifyOfflineSyncWaiter(fmt.Errorf("logged out: %s", evt.Reason))
case *events.Disconnected: case *events.Disconnected:
// Don't send the normal transient disconnect state if we're already in a different transient disconnect state. // Don't send the normal transient disconnect state if we're already in a different transient disconnect state.
@ -282,10 +284,9 @@ func (wa *WhatsAppClient) ensureAltJIDs(ctx context.Context, info *types.Message
func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) { func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) {
success = true success = true
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast { if (evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast) ||
return (evt.Info.Chat.Server == types.NewsletterServer && wa.disableNewsletter) ||
} (evt.Info.Chat.Server == types.BroadcastServer && evt.Info.IsFromMe) {
if evt.Info.Chat.Server == types.NewsletterServer && wa.disableNewsletter {
return return
} }
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) {
@ -333,7 +334,15 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
wa.Main.Bridge.Config.Backfill.Enabled { wa.Main.Bridge.Config.Backfill.Enabled {
wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification) wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification)
} }
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") { if parsedMessageType == "ignore" {
return
} else if strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
wa.UserLogin.Log.Debug().
Str("message_id", evt.Info.ID).
Stringer("chat_jid", evt.Info.Chat).
Stringer("sender_jid", evt.Info.Sender).
Stringer("protocol_message_type", evt.Message.GetProtocolMessage().GetType()).
Msg("Ignoring unknown protocol message")
return return
} }
@ -341,18 +350,8 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation() messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation()
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 := evt.Message.GetProtocolMessage() protocolMsg, shouldHideEdit := makeHDMediaReplacementEdit(evt.Message, parentKey)
if protocolMsg.GetType() != waE2E.ProtocolMessage_MESSAGE_EDIT || protocolMsg.GetKey() == nil { dontRenderEdited = shouldHideEdit
protocolMsg = &waE2E.ProtocolMessage{
Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(),
Key: parentKey,
EditedMessage: evt.Message.GetAssociatedChildMessage().GetMessage(),
}
dontRenderEdited = true
} else if child := protocolMsg.GetEditedMessage().GetAssociatedChildMessage().GetMessage(); child != nil {
protocolMsg.EditedMessage = child
protocolMsg.Key = parentKey
}
wa.UserLogin.Log.Debug(). wa.UserLogin.Log.Debug().
Str("message_id", evt.Info.ID). Str("message_id", evt.Info.ID).
Str("parent_id", parentKey.GetID()). Str("parent_id", parentKey.GetID()).
@ -386,6 +385,26 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
return res.Success return res.Success
} }
func makeHDMediaReplacementEdit(message *waE2E.Message, parentKey *waCommon.MessageKey) (*waE2E.ProtocolMessage, bool) {
protocolMsg := message.GetProtocolMessage()
associatedMessage := message.GetAssociatedChildMessage().GetMessage()
if protocolMsg.GetType() != waE2E.ProtocolMessage_MESSAGE_EDIT || protocolMsg.GetKey() == nil {
protocolMsg = associatedMessage.GetProtocolMessage()
}
if protocolMsg.GetType() == waE2E.ProtocolMessage_MESSAGE_EDIT && protocolMsg.GetKey() != nil {
if child := protocolMsg.GetEditedMessage().GetAssociatedChildMessage().GetMessage(); child != nil {
protocolMsg.EditedMessage = child
}
protocolMsg.Key = parentKey
return protocolMsg, false
}
return &waE2E.ProtocolMessage{
Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(),
Key: parentKey,
EditedMessage: associatedMessage,
}, true
}
func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool { func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool {
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) {
return false return false
@ -465,6 +484,8 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
messageSender := wa.GetLID() messageSender := wa.GetLID()
if !evt.MessageSender.IsEmpty() { if !evt.MessageSender.IsEmpty() {
messageSender = evt.MessageSender messageSender = evt.MessageSender
} else if evt.Chat.Server == types.NewsletterServer {
messageSender = evt.Chat
} }
var chatAlt types.JID var chatAlt types.JID
if evt.Chat.Server == types.DefaultUserServer { if evt.Chat.Server == types.DefaultUserServer {
@ -483,6 +504,8 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
senderLID := evt.Sender senderLID := evt.Sender
if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() { if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() {
senderLID = evt.SenderAlt senderLID = evt.SenderAlt
} else if evt.Chat.Server == types.NewsletterServer && evt.Type == types.ReceiptTypeReadSelf {
senderLID = wa.GetLID()
} }
res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{ res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
@ -490,6 +513,11 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
PortalKey: wa.makeWAPortalKey(evt.Chat), PortalKey: wa.makeWAPortalKey(evt.Chat),
Sender: wa.makeEventSender(ctx, senderLID), Sender: wa.makeEventSender(ctx, senderLID),
Timestamp: evt.Timestamp, Timestamp: evt.Timestamp,
LogContext: func(c zerolog.Context) zerolog.Context {
return c.
Strs("targets", exslices.CastToString[string](targets)).
Stringer("receipt_sender", senderLID)
},
}, },
Targets: targets, Targets: targets,
}) })
@ -528,7 +556,7 @@ func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events.
}) })
} }
func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onConnect bool) { func (wa *WhatsAppClient) handleWALogout(ctx context.Context, reason events.ConnectFailureReason, onConnect bool) {
errorCode := WAUnknownLogout errorCode := WAUnknownLogout
if reason == events.ConnectFailureLoggedOut { if reason == events.ConnectFailureLoggedOut {
errorCode = WALoggedOut errorCode = WALoggedOut
@ -540,6 +568,10 @@ func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onC
wa.JID = types.EmptyJID wa.JID = types.EmptyJID
wa.LID = types.EmptyJID wa.LID = types.EmptyJID
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0 wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0
err := wa.Main.DB.Conversation.DeleteAll(ctx, wa.UserLogin.ID)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete history sync data on logout")
}
wa.UserLogin.BridgeState.Send(status.BridgeState{ wa.UserLogin.BridgeState.Send(status.BridgeState{
StateEvent: status.StateBadCredentials, StateEvent: status.StateBadCredentials,
Error: errorCode, Error: errorCode,
@ -704,13 +736,13 @@ func (wa *WhatsAppClient) syncGhost(jid types.JID, reason string, pictureID *str
if pictureID != nil && *pictureID != "" && ghost.AvatarID == networkid.AvatarID(*pictureID) { if pictureID != nil && *pictureID != "" && ghost.AvatarID == networkid.AvatarID(*pictureID) {
return return
} }
userInfo, err := wa.getUserInfo(ctx, jid, pictureID != nil) userInfo, err := wa.getUserInfo(ctx, jid, ptr.Val(pictureID), pictureID != nil)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to get user info") log.Err(err).Msg("Failed to get user info")
} else { } else {
ghost.UpdateInfo(ctx, userInfo) ghost.UpdateInfo(ctx, userInfo)
log.Debug().Msg("Synced ghost info") log.Debug().Msg("Synced ghost info")
wa.syncAltGhostWithInfo(ctx, jid, userInfo) wa.syncAltGhostWithInfo(ctx, jid, ghost)
} }
go wa.syncRemoteProfile(ctx, ghost) go wa.syncRemoteProfile(ctx, ghost)
} }

View file

@ -210,3 +210,60 @@ func (wa *WhatsAppConnector) migrateToLIDDMs(ctx context.Context) error {
wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_migrated", "true") wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_migrated", "true")
return nil 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
}
const findMismatchingGhosts = `
SELECT id
FROM ghost
WHERE bridge_id=$1
AND id LIKE 'lid-%'
AND avatar_mxc=''
AND EXISTS (
SELECT pnghost.avatar_mxc
FROM ghost pnghost
WHERE pnghost.bridge_id=$1
AND pnghost.id=(SELECT pn FROM whatsmeow_lid_map WHERE lid=replace(ghost.id, 'lid-', ''))
AND pnghost.avatar_mxc<>''
)
`
var scanGhostID = dbutil.ConvertRowFn[networkid.UserID](dbutil.ScanSingleColumn[networkid.UserID])
ghostIDs, err := scanGhostID.NewRowIter(wa.Bridge.DB.Query(ctx, findMismatchingGhosts, wa.Bridge.ID)).AsList()
if err != nil {
return fmt.Errorf("failed to get mismatching ghosts: %w", err)
}
for _, ghostID := range ghostIDs {
lid := waid.ParseUserID(ghostID)
pn, err := wa.DeviceStore.LIDMap.GetPNForLID(ctx, lid)
if err != nil {
return fmt.Errorf("failed to get PN for LID %s: %w", lid, err)
} else if pn.IsEmpty() {
zerolog.Ctx(ctx).Warn().Stringer("lid", lid).Msg("No PN for LID")
continue
}
pnGhost, err := wa.Bridge.GetGhostByID(ctx, waid.MakeUserID(pn))
if err != nil {
return fmt.Errorf("failed to get PN ghost for %s: %w", pn, err)
}
lidGhost, err := wa.Bridge.GetGhostByID(ctx, ghostID)
if err != nil {
return fmt.Errorf("failed to get LID ghost for %s: %w", ghostID, err)
}
if lidGhost.AvatarMXC != "" || pnGhost.AvatarMXC == "" {
continue
}
zerolog.Ctx(ctx).Debug().
Stringer("pn", pn).
Stringer("lid", lid).
Str("pn_ghost_avatar", string(pnGhost.AvatarMXC)).
Str("lid_ghost_avatar", string(lidGhost.AvatarMXC)).
Str("pn_ghost_name", pnGhost.Name).
Str("lid_ghost_name", lidGhost.Name).
Msg("Updating LID ghost avatar")
lidGhost.UpdateInfo(ctx, makeInfoFromGhost(pnGhost))
}
wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_avatars_resynced", "true")
return nil
}

View file

@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"sync/atomic" "sync/atomic"
"time" "time"
@ -98,6 +99,7 @@ func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User,
LoginComplete: exsync.NewEvent(), LoginComplete: exsync.NewEvent(),
PasskeyRequest: exsync.NewEvent(), PasskeyRequest: exsync.NewEvent(),
PasskeyConfirmation: exsync.NewEvent(), PasskeyConfirmation: exsync.NewEvent(),
ADVRotate: exsync.NewEvent(),
Received515: exsync.NewEvent(), Received515: exsync.NewEvent(),
}, nil }, nil
} }
@ -124,6 +126,8 @@ type WALogin struct {
PasskeyConfirmation *exsync.Event PasskeyConfirmation *exsync.Event
PasskeyConfirmationData *events.PairPasskeyConfirmation PasskeyConfirmationData *events.PairPasskeyConfirmation
ADVRotate *exsync.Event
Closed atomic.Bool Closed atomic.Bool
EventHandlerID uint32 EventHandlerID uint32
} }
@ -135,7 +139,8 @@ var (
_ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil) _ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil)
) )
const LoginConnectWait = 15 * time.Second const LoginConnectWait = 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) {
wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect) wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect)
@ -193,19 +198,21 @@ func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogi
} }
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) {
ctx, cancel := context.WithTimeout(ctx, LoginConnectWait)
defer cancel()
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, err return nil, err
} }
err = wl.WaitForQRs.Wait(ctx) connectCtx, cancelConnect := context.WithTimeout(ctx, LoginConnectWait)
err = wl.WaitForQRs.Wait(connectCtx)
cancelConnect()
if err != nil { if err != nil {
wl.Log.Warn().Err(err).Msg("Timed out waiting for connection") wl.Log.Warn().Err(err).Msg("Timed out waiting for connection")
return nil, fmt.Errorf("failed to wait for connection: %w", err) return nil, fmt.Errorf("failed to wait for connection: %w", err)
} }
pairingCode, err := wl.Client.PairPhone(ctx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)") pairCtx, cancelPair := context.WithTimeout(ctx, LoginPairPhoneWait)
defer cancelPair()
pairingCode, err := wl.Client.PairPhone(pairCtx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)")
if err != nil { if err != nil {
wl.Log.Err(err).Msg("Failed to request phone code login") wl.Log.Err(err).Msg("Failed to request phone code login")
if errors.Is(err, whatsmeow.ErrPhoneNumberTooShort) { if errors.Is(err, whatsmeow.ErrPhoneNumberTooShort) {
@ -267,6 +274,12 @@ func (wl *WALogin) handleEvent(rawEvt any) {
wl.StartTime = time.Now() wl.StartTime = time.Now()
wl.WaitForQRs.Set() wl.WaitForQRs.Set()
return return
case *events.RotateADVSecret:
wl.Log.Debug().Msg("Rotating ADV secret in all QRs")
for i, code := range wl.QRs {
wl.QRs[i] = strings.Replace(code, evt.OldSecret, evt.NewSecret, 1)
}
wl.ADVRotate.Set()
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
@ -329,6 +342,7 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
Int("current_index", currentIndex) Int("current_index", currentIndex)
if currentIndex > prevIndex { if currentIndex > prevIndex {
logEvt.Msg("Returning new QR immediately") logEvt.Msg("Returning new QR immediately")
wl.ADVRotate.Clear()
wl.PrevQRIndex.Store(int32(currentIndex)) wl.PrevQRIndex.Store(int32(currentIndex))
return makeQRStep(wl.QRs[currentIndex]), nil return makeQRStep(wl.QRs[currentIndex]), nil
} }
@ -340,6 +354,7 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
wl.Cancel() wl.Cancel()
return nil, ErrLoginTimeout return nil, ErrLoginTimeout
} }
wl.ADVRotate.Clear()
wl.PrevQRIndex.Store(int32(nextIndex)) wl.PrevQRIndex.Store(int32(nextIndex))
return makeQRStep(wl.QRs[nextIndex]), nil return makeQRStep(wl.QRs[nextIndex]), nil
case <-ctx.Done(): case <-ctx.Done():
@ -347,6 +362,10 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
return nil, ctx.Err() return nil, ctx.Err()
case <-wl.PasskeyRequest.GetChan(): case <-wl.PasskeyRequest.GetChan():
return wl.makePasskeyStep() return wl.makePasskeyStep()
case <-wl.ADVRotate.GetChan():
wl.Log.Debug().Msg("ADV secret was rotated, returning new QR immediately")
wl.ADVRotate.Clear()
return makeQRStep(wl.QRs[nextIndex]), nil
case <-wl.LoginComplete.GetChan(): case <-wl.LoginComplete.GetChan():
// continue // continue
} }

View file

@ -64,12 +64,12 @@ func looksEmaily(str string) bool {
return false return false
} }
type cacheEntry struct { type isOnWhatsappCacheEntry struct {
jid types.JID jid types.JID
ts time.Time ts time.Time
} }
var isOnWhatsappCache = exsync.NewMap[string, cacheEntry]() var isOnWhatsappCache = exsync.NewMap[string, isOnWhatsappCacheEntry]()
func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) (types.JID, error) { func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) (types.JID, error) {
if strings.HasSuffix(number, "@"+types.BotServer) || strings.HasSuffix(number, "@"+types.HiddenUserServer) { if strings.HasSuffix(number, "@"+types.BotServer) || strings.HasSuffix(number, "@"+types.HiddenUserServer) {
@ -94,7 +94,7 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string)
} else if !resp[0].IsIn { } else if !resp[0].IsIn {
return types.EmptyJID, bridgev2.WrapRespErr(fmt.Errorf("the server said +%s is not on WhatsApp", resp[0].JID.User), mautrix.MNotFound) return types.EmptyJID, bridgev2.WrapRespErr(fmt.Errorf("the server said +%s is not on WhatsApp", resp[0].JID.User), mautrix.MNotFound)
} else { } else {
isOnWhatsappCache.Set(number, cacheEntry{resp[0].JID, time.Now()}) isOnWhatsappCache.Set(number, isOnWhatsappCacheEntry{resp[0].JID, time.Now()})
return resp[0].JID, nil return resp[0].JID, nil
} }
} }
@ -176,11 +176,16 @@ func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier stri
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get ghost: %w", err) return nil, fmt.Errorf("failed to get ghost: %w", err)
} }
userInfo, err := wa.getUserInfo(ctx, jid, "", false)
if err != nil {
return nil, fmt.Errorf("failed to get user info: %w", err)
}
return &bridgev2.ResolveIdentifierResponse{ return &bridgev2.ResolveIdentifierResponse{
Ghost: ghost, Ghost: ghost,
UserID: waid.MakeUserID(jid), UserID: waid.MakeUserID(jid),
Chat: wa.makeCreateChatResponse(ctx, jid, origJID), UserInfo: userInfo,
Chat: wa.makeCreateChatResponse(ctx, jid, origJID),
}, nil }, nil
} }
@ -238,7 +243,7 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl
resp = append(resp, &bridgev2.ResolveIdentifierResponse{ resp = append(resp, &bridgev2.ResolveIdentifierResponse{
Ghost: ghost, Ghost: ghost,
UserID: waid.MakeUserID(jid), UserID: waid.MakeUserID(jid),
UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, false), UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, "", false),
Chat: chatResp, Chat: chatResp,
}) })
} }

View file

@ -165,13 +165,13 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID
log.Warn().Stringer("jid", jid).Msg("Didn't get info for puppet in background sync") log.Warn().Stringer("jid", jid).Msg("Didn't get info for puppet in background sync")
continue continue
} }
userInfo, err := wa.getUserInfo(ctx, jid, info.PictureID != "" && string(ghost.AvatarID) != info.PictureID) userInfo, err := wa.getUserInfo(ctx, jid, info.PictureID, info.PictureID != "" && string(ghost.AvatarID) != info.PictureID)
if err != nil { if err != nil {
log.Err(err).Stringer("jid", jid).Msg("Failed to get user info for puppet in background sync") log.Err(err).Stringer("jid", jid).Msg("Failed to get user info for puppet in background sync")
continue continue
} }
ghost.UpdateInfo(ctx, userInfo) ghost.UpdateInfo(ctx, userInfo)
wa.syncAltGhostWithInfo(ctx, jid, userInfo) wa.syncAltGhostWithInfo(ctx, jid, ghost)
} }
} }
@ -181,18 +181,18 @@ func (wa *WhatsAppClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost
return nil, nil return nil, nil
} }
jid := waid.ParseUserID(ghost.ID) jid := waid.ParseUserID(ghost.ID)
return wa.getUserInfo(ctx, jid, ghost.AvatarID == "") return wa.getUserInfo(ctx, jid, "", ghost.AvatarID == "")
} }
func (wa *WhatsAppClient) getUserInfo(ctx context.Context, jid types.JID, fetchAvatar bool) (*bridgev2.UserInfo, error) { func (wa *WhatsAppClient) getUserInfo(ctx context.Context, jid types.JID, avatarID string, fetchAvatar bool) (*bridgev2.UserInfo, error) {
contact, err := wa.GetStore().Contacts.GetContact(ctx, jid) contact, err := wa.GetStore().Contacts.GetContact(ctx, jid)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return wa.contactToUserInfo(ctx, jid, contact, fetchAvatar), nil return wa.contactToUserInfo(ctx, jid, contact, avatarID, fetchAvatar), nil
} }
func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, contact types.ContactInfo, getAvatar bool) *bridgev2.UserInfo { func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, contact types.ContactInfo, avatarID string, fetchAvatar bool) *bridgev2.UserInfo {
if jid == types.MetaAIJID && contact.PushName == jid.User { if jid == types.MetaAIJID && contact.PushName == jid.User {
contact.PushName = "Meta AI" contact.PushName = "Meta AI"
} else if jid == types.LegacyPSAJID || jid == types.PSAJID { } else if jid == types.LegacyPSAJID || jid == types.PSAJID {
@ -270,7 +270,9 @@ func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID,
} else if phone != "" { } else if phone != "" {
ui.Identifiers = []string{fmt.Sprintf("tel:%s", phone)} ui.Identifiers = []string{fmt.Sprintf("tel:%s", phone)}
} }
if getAvatar { if wa.Main.Config.LazyAvatars {
ui.ExtraUpdates = bridgev2.MergeExtraUpdaters(ui.ExtraUpdates, wa.makeLazyGhostAvatarUpdater(avatarID, fetchAvatar))
} else if fetchAvatar {
ui.ExtraUpdates = bridgev2.MergeExtraUpdaters(ui.ExtraUpdates, wa.fetchGhostAvatar) ui.ExtraUpdates = bridgev2.MergeExtraUpdaters(ui.ExtraUpdates, wa.fetchGhostAvatar)
} }
return ui return ui
@ -307,6 +309,21 @@ func avatarInfoToCacheEntry(ctx context.Context, jid types.JID, avatar *types.Pr
} }
} }
func (wa *WhatsAppClient) makeLazyDirectMediaAvatar(ctx context.Context, jid types.JID, avatarID string, community bool) (*bridgev2.Avatar, error) {
if avatarID == "" {
avatarID = waid.MakeRandomAvatarID()
}
mxc, err := wa.Main.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeAvatarMediaID(jid, avatarID, wa.UserLogin.ID, community))
if err != nil {
return nil, fmt.Errorf("failed to generate MXC URI: %w", err)
}
return &bridgev2.Avatar{
ID: networkid.AvatarID(avatarID),
MXC: mxc,
Hash: sha256.Sum256([]byte(avatarID)),
}, nil
}
func (wa *WhatsAppClient) makeDirectMediaAvatar(ctx context.Context, jid types.JID, avatar *types.ProfilePictureInfo, community bool) (*bridgev2.Avatar, error) { func (wa *WhatsAppClient) makeDirectMediaAvatar(ctx context.Context, jid types.JID, avatar *types.ProfilePictureInfo, community bool) (*bridgev2.Avatar, error) {
mxc, err := wa.Main.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeAvatarMediaID(jid, avatar.ID, wa.UserLogin.ID, community)) mxc, err := wa.Main.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeAvatarMediaID(jid, avatar.ID, wa.UserLogin.ID, community))
if err != nil { if err != nil {
@ -328,6 +345,21 @@ func (wa *WhatsAppClient) makeDirectMediaAvatar(ctx context.Context, jid types.J
}, nil }, nil
} }
func (wa *WhatsAppClient) makeLazyGhostAvatarUpdater(avatarID string, forceUpdate bool) func(context.Context, *bridgev2.Ghost) bool {
return func(ctx context.Context, ghost *bridgev2.Ghost) bool {
if ghost.AvatarID != "" && (networkid.AvatarID(avatarID) == ghost.AvatarID || (!forceUpdate && avatarID == "")) {
return false
}
jid := waid.ParseUserID(ghost.ID)
wrappedAvatar, err := wa.makeLazyDirectMediaAvatar(ctx, jid, avatarID, false)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to prepare lazy direct media avatar")
return false
}
return ghost.UpdateAvatar(ctx, wrappedAvatar)
}
}
func (wa *WhatsAppClient) fetchGhostAvatar(ctx context.Context, ghost *bridgev2.Ghost) bool { func (wa *WhatsAppClient) fetchGhostAvatar(ctx context.Context, ghost *bridgev2.Ghost) bool {
jid := waid.ParseUserID(ghost.ID) jid := waid.ParseUserID(ghost.ID)
existingID := string(ghost.AvatarID) existingID := string(ghost.AvatarID)
@ -399,14 +431,14 @@ func (wa *WhatsAppClient) resyncContacts(forceAvatarSync, automatic bool) {
} else if contact, err := contactStore.GetContact(ctx, jid); err != nil { } else if contact, err := contactStore.GetContact(ctx, jid); err != nil {
log.Err(err).Stringer("jid", jid).Msg("Failed to get contact info") log.Err(err).Stringer("jid", jid).Msg("Failed to get contact info")
} else { } else {
userInfo := wa.contactToUserInfo(ctx, jid, contact, forceAvatarSync || ghost.AvatarID == "") userInfo := wa.contactToUserInfo(ctx, jid, contact, "", forceAvatarSync || ghost.AvatarID == "")
ghost.UpdateInfo(ctx, userInfo) ghost.UpdateInfo(ctx, userInfo)
wa.syncAltGhostWithInfo(ctx, jid, userInfo) wa.syncAltGhostWithInfo(ctx, jid, ghost)
} }
} }
} }
func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JID, info *bridgev2.UserInfo) { func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JID, mainGhost *bridgev2.Ghost) {
log := zerolog.Ctx(ctx) log := zerolog.Ctx(ctx)
var altJID types.JID var altJID types.JID
var err error var err error
@ -431,10 +463,25 @@ func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JI
Msg("Failed to get ghost for alternate JID") Msg("Failed to get ghost for alternate JID")
return return
} }
ghost.UpdateInfo(ctx, info) ghost.UpdateInfo(ctx, makeInfoFromGhost(mainGhost))
log.Debug(). log.Debug().
Stringer("jid", jid). Stringer("jid", jid).
Stringer("alternate_jid", altJID). Stringer("alternate_jid", altJID).
Msg("Synced alternate ghost with info") Msg("Synced alternate ghost with info")
go wa.syncRemoteProfile(ctx, ghost) go wa.syncRemoteProfile(ctx, ghost)
} }
func makeInfoFromGhost(ghost *bridgev2.Ghost) *bridgev2.UserInfo {
return &bridgev2.UserInfo{
Identifiers: ghost.Identifiers,
Name: &ghost.Name,
Avatar: &bridgev2.Avatar{
ID: ghost.AvatarID,
Remove: ghost.AvatarID == "" || ghost.AvatarMXC == "",
MXC: ghost.AvatarMXC,
Hash: ghost.AvatarHash,
},
IsBot: &ghost.IsBot,
ExtraProfile: ghost.ExtraProfile,
}
}

View file

@ -2,10 +2,14 @@ package wadb
import ( import (
"context" "context"
"strings"
"time"
"go.mau.fi/util/dbutil" "go.mau.fi/util/dbutil"
"go.mau.fi/util/jsontime" "go.mau.fi/util/jsontime"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
) )
type AvatarCacheQuery struct { type AvatarCacheQuery struct {
@ -49,3 +53,13 @@ func (ace *AvatarCacheEntry) Scan(row dbutil.Scannable) (*AvatarCacheEntry, erro
func (ace *AvatarCacheEntry) sqlVariables() []any { func (ace *AvatarCacheEntry) sqlVariables() []any {
return []any{ace.EntityJID, ace.AvatarID, ace.DirectPath, ace.Expiry, ace.Gone} return []any{ace.EntityJID, ace.AvatarID, ace.DirectPath, ace.Expiry, ace.Gone}
} }
func (ace *AvatarCacheEntry) IsGone() bool {
return ace != nil && ace.Gone &&
// Random IDs can be retried after a specific expiry time, other types of gones can't
(ace.Expiry.IsZero() || !strings.HasPrefix(ace.AvatarID, waid.RandomAvatarIDPrefix) || ace.Expiry.After(time.Now()))
}
func (ace *AvatarCacheEntry) Expired() bool {
return ace == nil || ace.Expiry.Before(time.Now().Add(5*time.Minute))
}

View file

@ -190,7 +190,7 @@ func (mc *MessageConverter) ToMatrix(
case waMsg.KeepInChatMessage != nil: case waMsg.KeepInChatMessage != nil:
part, contextInfo = mc.convertKeepInChatMessage(ctx, waMsg.KeepInChatMessage) part, contextInfo = mc.convertKeepInChatMessage(ctx, waMsg.KeepInChatMessage)
case waMsg.RichResponseMessage != nil: case waMsg.RichResponseMessage != nil:
part, contextInfo = mc.convertRichResponseMessage(ctx, waMsg.RichResponseMessage) part, contextInfo = mc.convertRichResponseMessage(ctx, waMsg.RichResponseMessage, waMsg)
case waMsg.ImageMessage != nil: case waMsg.ImageMessage != nil:
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.ImageMessage, "photo", info, isViewOnce, previouslyConvertedPart) part, contextInfo = mc.convertMediaMessage(ctx, waMsg.ImageMessage, "photo", info, isViewOnce, previouslyConvertedPart)
case waMsg.StickerMessage != nil: case waMsg.StickerMessage != nil:

View file

@ -351,7 +351,7 @@ func (mc *MessageConverter) convertKeepInChatMessage(ctx context.Context, msg *w
}, nil }, nil
} }
func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage, fullMsg *waE2E.Message) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
var body strings.Builder var body strings.Builder
// TODO switch to new format? // TODO switch to new format?
@ -363,10 +363,14 @@ func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg
body.WriteString(submsg.GetMessageText()) body.WriteString(submsg.GetMessageText())
} }
} }
unknownPart, _ := mc.convertUnknownMessage(ctx, fullMsg)
if body.Len() == 0 {
return unknownPart, msg.GetContextInfo()
}
content := format.RenderMarkdown(body.String(), true, false) content := format.RenderMarkdown(body.String(), true, false)
return &bridgev2.ConvertedMessagePart{ return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage, Type: event.EventMessage,
Content: &content, Content: &content,
Extra: unknownPart.Extra,
}, msg.GetContextInfo() }, msg.GetContextInfo()
} }

View file

@ -37,12 +37,12 @@ type UserLoginMetadata struct {
PushKeys *PushKeys `json:"push_keys,omitempty"` PushKeys *PushKeys `json:"push_keys,omitempty"`
APNSEncPubKey []byte `json:"apns_enc_pubkey,omitempty"` APNSEncPubKey []byte `json:"apns_enc_pubkey,omitempty"`
APNSEncPrivKey []byte `json:"apns_enc_privkey,omitempty"` APNSEncPrivKey []byte `json:"apns_enc_privkey,omitempty"`
LoggedInAt jsontime.Unix `json:"logged_in_at,omitempty"` LoggedInAt jsontime.Unix `json:"logged_in_at,omitzero"`
AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"` AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"`
HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"` HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitzero"`
ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitempty"` ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitzero"`
MData json.RawMessage `json:"mdata,omitempty"` MData json.RawMessage `json:"mdata,omitempty"`
} }
@ -88,13 +88,13 @@ type GroupInviteMeta struct {
} }
type MessageMetadata struct { type MessageMetadata struct {
SenderDeviceID uint16 `json:"sender_device_id,omitempty"` SenderDeviceID uint16 `json:"sender_device_id,omitzero"`
Error MessageErrorType `json:"error,omitempty"` Error MessageErrorType `json:"error,omitempty"`
BroadcastListJID *types.JID `json:"broadcast_list_jid,omitempty"` BroadcastListJID *types.JID `json:"broadcast_list_jid,omitempty"`
GroupInvite *GroupInviteMeta `json:"group_invite,omitempty"` GroupInvite *GroupInviteMeta `json:"group_invite,omitempty"`
FailedMediaMeta json.RawMessage `json:"media_meta,omitempty"` FailedMediaMeta json.RawMessage `json:"media_meta,omitempty"`
DirectMediaMeta json.RawMessage `json:"direct_media_meta,omitempty"` DirectMediaMeta json.RawMessage `json:"direct_media_meta,omitempty"`
IsMatrixPoll bool `json:"is_matrix_poll,omitempty"` IsMatrixPoll bool `json:"is_matrix_poll,omitzero"`
Edits []types.MessageID `json:"edits,omitempty"` Edits []types.MessageID `json:"edits,omitempty"`
} }
@ -122,14 +122,16 @@ type ReactionMetadata struct {
} }
type PortalMetadata struct { type PortalMetadata struct {
DisappearingTimerSetAt int64 `json:"disappearing_timer_set_at,omitempty"` DisappearingTimerSetAt int64 `json:"disappearing_timer_set_at,omitzero"`
TopicID string `json:"topic_id,omitempty"` TopicID string `json:"topic_id,omitempty"`
LastSync jsontime.Unix `json:"last_sync,omitempty"` LastSync jsontime.Unix `json:"last_sync,omitzero"`
CommunityAnnouncementGroup bool `json:"is_cag,omitempty"` CommunityAnnouncementGroup bool `json:"is_cag,omitzero"`
AddressingMode types.AddressingMode `json:"addressing_mode,omitempty"` AddressingMode types.AddressingMode `json:"addressing_mode,omitempty"`
LIDMigrationAttempted bool `json:"lid_migration_attempted,omitempty"` LIDMigrationAttempted bool `json:"lid_migration_attempted,omitzero"`
} }
type GhostMetadata struct { type GhostMetadata struct {
LastSync jsontime.Unix `json:"last_sync,omitempty"` LastSync jsontime.Unix `json:"last_sync,omitzero"`
DirectAvatarURL string `json:"direct_avatar_url,omitempty"`
} }

View file

@ -24,6 +24,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"go.mau.fi/util/random"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
) )
@ -85,6 +86,16 @@ type AvatarMediaInfo struct {
Community bool Community bool
} }
const RandomAvatarIDPrefix = "mxwa-lazy-avatar-"
func MakeRandomAvatarID() string {
return RandomAvatarIDPrefix + random.String(8)
}
func (ami *AvatarMediaInfo) IsRandom() bool {
return ami != nil && strings.HasPrefix(ami.AvatarID, RandomAvatarIDPrefix)
}
func MakeStickerPackMediaID(packID string, fileHash []byte, receiver networkid.UserLoginID) networkid.MediaID { func MakeStickerPackMediaID(packID string, fileHash []byte, receiver networkid.UserLoginID) networkid.MediaID {
receiverID := compactJID(ParseUserLoginID(receiver, 0)) receiverID := compactJID(ParseUserLoginID(receiver, 0))
mediaID := make([]byte, 0, 4+len(packID)+len(fileHash)+len(receiverID)) mediaID := make([]byte, 0, 4+len(packID)+len(fileHash)+len(receiverID))