diff --git a/.bridgelint.yaml b/.bridgelint.yaml deleted file mode 100644 index ea2b48b4..00000000 --- a/.bridgelint.yaml +++ /dev/null @@ -1 +0,0 @@ -code_format_allowlist: ["wa-connect-failure-%d"] diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 146b93a0..343443bd 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,14 +11,14 @@ jobs: strategy: fail-fast: false matrix: - go-version: ["1.26", "1.27"] - name: Lint ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }} + go-version: ["1.25", "1.26"] + name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v7 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} cache: true @@ -30,7 +30,6 @@ 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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1c96b066..1bfb5360 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,12 +21,9 @@ repos: - id: go-mod-tidy - repo: https://github.com/beeper/pre-commit-go - rev: v0.5.0 + rev: v0.4.2 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3002f927..e47e7b27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,50 +1,3 @@ -# v26.10 - -* Added support for new rich response messages used by the Muse AI bot. -* Added support for bridging custom profile set for Muse. -* Added automatic one-time resync of keys needed to decrypt Muse messages. -* Added support for blocking and unblocking users on WhatsApp. -* Added support for contact array messages (thanks to [@voska] in [#961]). -* Added partial support for filling quoted message when sending replies to - WhatsApp. -* Changed undecryptable message resends to only bridge as edits and never as new - messages, to avoid races if the message was deleted before the resend. -* Changed login to wait for OTKs to be successfully uploaded, as the login will - fail if the bridge is stopped before that point. -* Changed chat info syncing to not bridge mute status if app state sync is - failing. -* Changed power levels to allow poll votes in read-only groups even if the room - is encrypted. -* Fixed backfilled media retries not getting sent if configured for local time. -* Fixed "delete for me" from WhatsApp not being bridged correctly in DMs. -* Fixed very long WhatsApp messages not being bridged. -* Fixed document message caption edits not being handled correctly. - -[@voska]: https://github.com/voska -[#961]: https://github.com/mautrix/whatsapp/pull/961 - -# v26.09 - -* 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 - -* Switched direct chats to use LIDs instead of phone numbers. - # v26.07 * Updated Docker image to Alpine 3.24. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 764f753e..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1 +0,0 @@ -See diff --git a/cmd/mautrix-whatsapp/main.go b/cmd/mautrix-whatsapp/main.go index ded185c6..47d971cf 100644 --- a/cmd/mautrix-whatsapp/main.go +++ b/cmd/mautrix-whatsapp/main.go @@ -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.10", + Version: "26.07", SemCalVer: true, Connector: &connector.WhatsAppConnector{}, } diff --git a/go.mod b/go.mod index f940c1de..95219379 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,24 @@ module go.mau.fi/mautrix-whatsapp -go 1.26.0 +go 1.25.0 -toolchain go1.27.2 +toolchain go1.26.5 tool go.mau.fi/util/cmd/maubuild 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.20.0 - github.com/yuin/goldmark v1.8.6 - go.mau.fi/util v0.11.0 + github.com/tidwall/gjson v1.19.0 + go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 go.mau.fi/webp v0.3.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 + go.mau.fi/whatsmeow v0.0.0-20260805124304-6723e556f35a + golang.org/x/image v0.44.0 + golang.org/x/net v0.57.0 + golang.org/x/sync v0.22.0 + google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445 + maunium.net/go/mautrix v0.29.1-0.20260804113340-56938b8a508d ) require ( @@ -29,11 +27,12 @@ require ( github.com/coder/websocket v1.8.15 // indirect github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/kr/pretty v0.3.1 // indirect 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-20260918085751-abfca077860b // indirect + github.com/mattn/go-sqlite3 v1.14.48 // indirect + github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca // 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 @@ -41,13 +40,14 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect + github.com/yuin/goldmark v1.8.4 // indirect go.mau.fi/libsignal v0.2.2 // indirect go.mau.fi/zeroconfig v0.2.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 + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.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 diff --git a/go.sum b/go.sum index fb4e878f..322636bc 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ 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/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= 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/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -32,11 +34,13 @@ 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-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-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-20260918085751-abfca077860b h1:OzNsuVdSWGwXvWKTtChx9ve89k4cFJ6NxYM4Aw4/f+E= -github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= +github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= +github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca h1:GHSUVE4yOgX4E7kTRzpxCPbCOYkd3Kj8Dgdod30OI1E= +github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= 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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -48,11 +52,11 @@ 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/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/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= -github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto= -github.com/tidwall/gjson v1.20.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +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/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= @@ -63,37 +67,37 @@ 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/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/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= -github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= +github.com/yuin/goldmark v1.8.4/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.11.0 h1:JNEpkVRxfTdJUC0DIGKiRlLKzl4G/qLX/eVzoP7QuwY= -go.mau.fi/util v0.11.0/go.mod h1:04AEFVsc4zNdIkDC1gkdqbg/nZFolHY50pyXh2DSy7s= +go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 h1:lsvBEY8MJfYdV61YbwikiQvb0Al/onbmLW5wfl/0tag= +go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= 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-20261026125351-1fbae988179a h1:ZyCV/mWfRkutxs5R0K3v+oyTbF4huprSlPZg/LEFQ7k= -go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a/go.mod h1:iY4lm61nTLWsNiIE1EmHUZwhKGBeoV4dhnVE2uMtNoQ= +go.mau.fi/whatsmeow v0.0.0-20260805124304-6723e556f35a h1:TTRND9Dvq7YfT9vwJVQK4Zrit/sm3FHE74B5FWe8CgA= +go.mau.fi/whatsmeow v0.0.0-20260805124304-6723e556f35a/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= 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.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/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g= +golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= +golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= +golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= -golang.org/x/sys v0.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= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -103,5 +107,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.32.1-0.20261105141340-842796acf445 h1:ZI9Y2YEmjqQDloOCiciHeynTRYjUwK0ZZbNcRH/5NOc= -maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= +maunium.net/go/mautrix v0.29.1-0.20260804113340-56938b8a508d h1:jbC6tXphot/X1UngnZLv7LcDTzaHBi2phwxrpyhtfrs= +maunium.net/go/mautrix v0.29.1-0.20260804113340-56938b8a508d/go.mod h1:PeLuIih5jnbwb2xKNNi0Te7AtINEQE2Uv3p6iG3RV80= diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 8f006e0b..65e8f72c 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "runtime/debug" "slices" "strconv" "strings" @@ -13,8 +12,6 @@ import ( "time" "github.com/rs/zerolog" - "go.mau.fi/util/exerrors" - "go.mau.fi/util/exmaps" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waE2E" @@ -64,7 +61,7 @@ func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) { } }() for { - var resetTimer, errored bool + var resetTimer bool select { case <-wa.historySyncWakeup: dispatchTimer.Stop() @@ -74,21 +71,12 @@ func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) { } else if notif == nil { wa.UserLogin.Log.Debug().Msg("No more queued history sync notifications") } else { - resetTimer, errored = wa.downloadAndSaveWAHistorySyncData(ctx, notif, rowid) + resetTimer = wa.downloadAndSaveWAHistorySyncData(ctx, notif, rowid) // Continue waking up the loop until all queued notifications are processed select { case wa.historySyncWakeup <- struct{}{}: default: } - if errored { - wa.UserLogin.Log.Debug().Msg("Temporarily stopping history sync loop after error") - select { - case <-time.After(1 * time.Minute): - case <-ctx.Done(): - wa.UserLogin.Log.Debug().Msg("Stopping main history sync loop") - return - } - } } case <-ctx.Done(): wa.UserLogin.Log.Debug().Msg("Stopping main history sync loop") @@ -120,21 +108,13 @@ func (wa *WhatsAppClient) saveWAHistorySyncNotification(ctx context.Context, evt } } -func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, evt *waE2E.HistorySyncNotification, rowid int) (resetTimer, errored bool) { +func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, evt *waE2E.HistorySyncNotification, rowid int) (resetTimer bool) { log := wa.UserLogin.Log.With(). Str("action", "download history sync"). Stringer("sync_type", evt.GetSyncType()). Uint32("chunk_order", evt.GetChunkOrder()). Uint32("progress", evt.GetProgress()). Logger() - defer func() { - if v := recover(); v != nil { - log.Err(exerrors.RecoverToError(v)). - Bytes(zerolog.ErrorStackFieldName, debug.Stack()). - Msg("History sync saving panicked") - errored = true - } - }() log.Debug(). Int64("oldest_msg_in_chunk_ts", evt.GetOldestMsgInChunkTimestampSec()). Any("full_request_meta", evt.GetFullHistorySyncOnDemandRequestMetadata()). @@ -142,20 +122,11 @@ 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 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 { + 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") @@ -179,7 +150,6 @@ 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 || @@ -255,17 +225,10 @@ 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 == types.MetaAIJID { - jid = types.NewMetaAIJID - } else if jid.Server == types.DefaultUserServer { + 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") @@ -283,14 +246,12 @@ func (wa *WhatsAppClient) handleWAHistorySync( return c.Stringer("chat_jid", jid) }) - var firstItemTime, lastItemTime time.Time + var minTime, maxTime, firstItemTime, lastItemTime time.Time + var minTimeIndex, maxTimeIndex int ignoredTypes := 0 - rawMessages := conv.GetMessages() - messages := make([]*wadb.HistorySyncMessageTuple, 0, len(rawMessages)) - allowClamp := conv.GetCommentsCount() == 0 - var newerTS uint64 - for i, rawMsg := range rawMessages { + messages := make([]*wadb.HistorySyncMessageTuple, 0, len(conv.GetMessages())) + for i, rawMsg := range conv.GetMessages() { // Don't store messages that will just be skipped. msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage()) if err != nil { @@ -298,9 +259,6 @@ func (wa *WhatsAppClient) handleWAHistorySync( 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()). @@ -311,30 +269,20 @@ func (wa *WhatsAppClient) handleWAHistorySync( firstItemTime = 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) if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") { ignoredTypes++ 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) if err != nil { log.Warn().Err(err). @@ -349,8 +297,13 @@ func (wa *WhatsAppClient) handleWAHistorySync( log.Debug(). Int("wrapped_count", len(messages)). 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("last_item_time", lastItemTime). + Bool("highest_time_mismatch", firstItemTime != maxTime). Dict("metadata", zerolog.Dict(). Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()). Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()). @@ -366,7 +319,7 @@ func (wa *WhatsAppClient) handleWAHistorySync( Msg("Collected messages to save from history sync conversation") if len(messages) > 0 { - err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, firstItemTime)) + err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime)) if err != nil { if stopOnError { return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err) @@ -406,13 +359,6 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) { log := wa.UserLogin.Log.With(). Str("action", "create portals from history sync"). Logger() - defer func() { - if v := recover(); v != nil { - log.Err(exerrors.RecoverToError(v)). - Bytes(zerolog.ErrorStackFieldName, debug.Stack()). - Msg("History sync portal creation panicked") - } - }() ctx = log.WithContext(ctx) limit := wa.Main.Config.HistorySync.MaxInitialConversations loginTS := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).LoggedInAt @@ -454,7 +400,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) || errors.Is(err, ErrNewsletterNotFound) { + if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) { 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) @@ -578,7 +524,6 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet 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) { wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0) - hasMore = hasMore && params.AnchorMessage != nil if hasMore && !params.AllowSlowFetch { return &bridgev2.FetchMessagesResponse{ MoreRequiresSlowFetch: true, @@ -605,36 +550,16 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet } } } - resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true, !params.Forward && !hasMore) + resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true) if err != nil { 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 @@ -667,19 +592,13 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( portalJID types.JID, messages []*waWeb.WebMessageInfo, explodeOnError bool, - deleteEverything bool, ) (*bridgev2.FetchMessagesResponse, error) { - cli := wa.Client - if cli == nil { - return nil, bridgev2.ErrNotLoggedIn - } oldestTS := messages[len(messages)-1].GetMessageTimestamp() newestTS := messages[0].GetMessageTimestamp() convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages)) var mediaRequests []*wadb.MediaRequest - dups := make(exmaps.Set[networkid.MessageID]) for i, msg := range messages { - evt, err := cli.ParseWebMessage(portalJID, msg) + evt, err := wa.Client.ParseWebMessage(portalJID, msg) if err != nil { if explodeOnError { // This should never happen because the info is already parsed once before being stored in the database @@ -704,18 +623,10 @@ 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, + ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, ) - // This is a hack to remove duplicates where the same message is inserted with both the LID and phone number sender - // TODO prevent those being inserted in the first place instead of hacking around it here - if !dups.Add(converted.ID) { - continue - } convertedMessages = append(convertedMessages, converted) if mediaReq != nil { mediaRequests = append(mediaRequests, mediaReq) @@ -726,11 +637,9 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( Messages: convertedMessages, Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)), CompleteCallback: func() { - if deleteEverything { - wa.deleteHistorySyncMessages(ctx, portalJID, 1<<62, oldestTS) - } else { - wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS) - } + // 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 len(mediaRequests) > 0 { go func(ctx context.Context) { for _, req := range mediaRequests { @@ -752,28 +661,12 @@ 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") } - anchor := params.AnchorMessage - parsed, err := waid.ParseMessageID(anchor.ID) + parsed, err := waid.ParseMessageID(params.AnchorMessage.ID) if err != nil { - 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) - } + 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, @@ -782,7 +675,7 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID IsGroup: parsed.Chat.Server == types.GroupServer, }, ID: parsed.ID, - Timestamp: anchor.Timestamp, + Timestamp: params.AnchorMessage.Timestamp, }, 50) zerolog.Ctx(ctx).Debug(). Str("request_msg_id", msgID). @@ -795,9 +688,6 @@ 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{ @@ -806,38 +696,6 @@ 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(). @@ -850,9 +708,7 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID") continue } - if portalJID == types.MetaAIJID { - portalJID = types.NewMetaAIJID - } else if portalJID.Server == types.DefaultUserServer { + 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") @@ -893,7 +749,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, false) + resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false) if err != nil { return nil, err } @@ -905,7 +761,7 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w } func (wa *WhatsAppClient) convertHistorySyncMessage( - ctx context.Context, cli *whatsmeow.Client, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction, + ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction, ) (*bridgev2.BackfillMessage, *wadb.MediaRequest) { // New messages turn these into edits, but in backfill we only have the last version, // so no need to do the edit thing. Instead, just unwrap the message. @@ -916,7 +772,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessage( intent := wa.Main.Bridge.Bot msgID := waid.MakeMessageIDWithAltSender(info.Chat, info.Sender, info.SenderAlt, info.ID) wrapped := &bridgev2.BackfillMessage{ - ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, cli, intent, msg, rawMsg, info, isViewOnce, true, nil), + ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil), Sender: wa.makeEventSender(ctx, pickLID(info.Sender, info.SenderAlt)), ID: msgID, TxnID: networkid.TransactionID(msgID), diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index b991893b..152e7ad6 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -52,7 +52,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit } func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) { - return 1, 10 + return 1, 9 } const WAMaxFileSize = 2000 * 1024 * 1024 @@ -67,7 +67,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel { } func capID() string { - base := "fi.mau.whatsapp.capabilities.2026_10_01" + base := "fi.mau.whatsapp.capabilities.2026_07_22" if ffmpeg.Supported() { return base + "+ffmpeg" } @@ -198,7 +198,6 @@ var whatsappCaps = &event.RoomFeatures{ var whatsappDMCaps *event.RoomFeatures var whatsappCAGCaps *event.RoomFeatures -var whatsappNoteToSelfCaps *event.RoomFeatures func init() { whatsappDMCaps = ptr.Clone(whatsappCaps) @@ -206,13 +205,7 @@ 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 @@ -223,10 +216,6 @@ 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 diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index ce8c7396..1dbebcc4 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -10,7 +10,6 @@ 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,21 +29,15 @@ func (wa *WhatsAppClient) GetChatInfo(ctx context.Context, portal *bridgev2.Port return wa.getChatInfo(ctx, portalJID, nil, portal.MXID == "") } -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 { case types.DefaultUserServer, types.HiddenUserServer, types.BotServer: wrapped = wa.wrapDMInfo(ctx, portalJID) - if portalJID == types.MuseJID { - go wa.requestMuseProfile(ctx) - } case types.BroadcastServer: if portalJID == types.StatusBroadcastJID { wrapped = wa.wrapStatusBroadcastInfo(ctx) } else { - return nil, ErrBroadcastList + return nil, fmt.Errorf("broadcast list bridging is currently not supported") } case types.GroupServer: info, err := wa.Client.GetGroupInfo(ctx, portalJID) @@ -57,8 +50,6 @@ 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: @@ -120,16 +111,9 @@ func (wa *WhatsAppClient) applyChatSettings(ctx context.Context, chatID types.JI return } } - 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 = &bridgev2.UserLocalPortalInfo{ + MutedUntil: ptr.Ptr(chat.MutedUntil), } - info.UserLocal = userLocal if chat.Pinned { info.UserLocal.Tag = ptr.Ptr(wa.Main.Config.PinnedTag) } else if chat.Archived { @@ -196,11 +180,6 @@ 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) } @@ -279,8 +258,6 @@ 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 } @@ -294,12 +271,11 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn setAddressingMode(info.AddressingMode), setTopicID(info.TopicID, info.Topic), ) - syncAllMembers := wa.Main.Config.MaxMemberSync < 0 || len(info.Participants) < wa.Main.Config.MaxMemberSync wrapped := &bridgev2.ChatInfo{ Name: ptr.Ptr(info.Name), Topic: ptr.Ptr(info.Topic), Members: &bridgev2.ChatMemberList{ - IsFull: !info.IsIncognito && !info.IsParent && syncAllMembers, + IsFull: !info.IsIncognito && !info.IsParent, TotalMemberCount: len(info.Participants), MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)), PowerLevels: &bridgev2.PowerLevelOverrides{ @@ -308,14 +284,14 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn Ban: ptr.Ptr(nobodyPL), // TODO allow invites if bridge config says to allow them, or maybe if relay mode is enabled? Events: map[event.Type]int{ - 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, + event.StateRoomName: metaChangePL, + event.StateRoomAvatar: metaChangePL, + event.StateTopic: metaChangePL, + event.EventReaction: defaultPL, + event.EventRedaction: defaultPL, + event.StateBeeperDisappearingTimer: metaChangePL, + // TODO always allow poll responses }, }, }, @@ -340,9 +316,6 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn } else if pcp.IsAdmin { member.PowerLevel = ptr.Ptr(adminPL) } else { - if !syncAllMembers && !member.EventSender.IsFromMe { - continue - } member.PowerLevel = ptr.Ptr(defaultPL) } member.MemberEventExtra = map[string]any{ @@ -583,27 +556,10 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne event.StateTopic: adminPL, event.EventReaction: defaultPL, event.EventRedaction: defaultPL, - // TODO support poll votes in channels - //event.EventUnstablePollResponse: defaultPL, + // TODO always allow poll responses }, }, }, 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 -} diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 678c8d22..c5a9348b 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -54,12 +54,8 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. directMediaRetries: make(map[networkid.MessageID]*directMediaRetry), mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle), pushNamesSynced: exsync.NewEvent(), + createDedup: exsync.NewSet[types.MessageID](), 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, } login.Client = w @@ -81,11 +77,10 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. w.Client = whatsmeow.NewClient(w.Device, waLog.Zerolog(log)) w.Client.AddEventHandlerWithSuccessStatus(w.handleWAEvent) w.Client.SynchronousAck = true - w.Client.EnableDecryptedEventBuffer = wa.Bridge.Config.PortalEventBuffer == 0 + w.Client.EnableDecryptedEventBuffer = bridgev2.PortalEventBuffer == 0 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) @@ -120,28 +115,15 @@ type WhatsAppClient struct { nextResync time.Time directMediaRetries map[networkid.MessageID]*directMediaRetry directMediaLock sync.Mutex - avatarLock exsync.KeyedMutex[types.JID] mediaRetryLock *semaphore.Weighted offlineSyncWaiter atomic.Pointer[chan error] isNewLogin bool pushNamesSynced *exsync.Event lastPresence types.Presence - blocklistLock sync.Mutex - - disableNewsletter bool + createDedup *exsync.Set[types.MessageID] appStateRecoveryLock sync.Mutex appStateFullSyncAttempted map[appstate.WAPatchName]time.Time - - 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 ( @@ -210,26 +192,7 @@ 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) || - 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") - } + (jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User) } func (wa *WhatsAppClient) GetLID() types.JID { @@ -240,17 +203,13 @@ func (wa *WhatsAppClient) GetLID() types.JID { } func (wa *WhatsAppClient) Connect(ctx context.Context) { - wa.connect(ctx) -} - -func (wa *WhatsAppClient) connect(ctx context.Context) bool { if wa.Client == nil { state := status.BridgeState{ StateEvent: status.StateBadCredentials, Error: WANotLoggedIn, } wa.UserLogin.BridgeState.Send(state) - return false + return } wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting}) wa.Main.firstClientConnectOnce.Do(wa.Main.onFirstClientConnect) @@ -258,7 +217,7 @@ func (wa *WhatsAppClient) connect(ctx context.Context) bool { zerolog.Ctx(ctx).Err(err).Msg("Failed to update proxy") } if ctx.Err() != nil { - return false + return } wa.initMC() wa.startLoops() @@ -275,9 +234,7 @@ func (wa *WhatsAppClient) connect(ctx context.Context) bool { }, } wa.UserLogin.BridgeState.Send(state) - return false } - return true } func (wa *WhatsAppClient) notifyOfflineSyncWaiter(err error) { @@ -434,10 +391,6 @@ func (wa *WhatsAppClient) LogoutRemote(ctx context.Context) { } wa.Disconnect() 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 { @@ -494,10 +447,6 @@ func (wa *WhatsAppClient) HandleMatrixViewingChat(ctx context.Context, msg *brid } } - if msg.Portal != nil && msg.Portal.ID == waid.MakePortalID(types.MuseJID) { - go wa.requestMuseProfile(ctx) - } - if msg.Portal == nil || msg.Portal.Metadata.(*waid.PortalMetadata).LastSync.Add(5*time.Minute).After(time.Now()) { // If we resynced this portal within the last 5 minutes, don't do it again return nil diff --git a/pkg/connector/commands.go b/pkg/connector/commands.go index 120d05ee..037d24e2 100644 --- a/pkg/connector/commands.go +++ b/pkg/connector/commands.go @@ -148,7 +148,13 @@ func fnSync(ce *commands.Event) { case "appstate": names := appstate.AllPatchNames[:] if len(ce.Args) > 1 { - names = parseAppStateNames(ce, ce.Args[1:]) + names = exslices.CastFuncFilter(ce.Args[1:], func(name string) (appstate.WAPatchName, bool) { + if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) { + ce.Reply("Invalid app state name `%s`", name) + return "", false + } + return appstate.WAPatchName(name), true + }) } for _, name := range names { err := wa.Client.FetchAppState(ce.Ctx, name, true, false) @@ -168,50 +174,6 @@ func fnSync(ce *commands.Event) { } } -func parseAppStateNames(ce *commands.Event, args []string) []appstate.WAPatchName { - return exslices.CastFuncFilter(args, func(name string) (appstate.WAPatchName, bool) { - if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) { - ce.Reply("Invalid app state name `%s`", name) - return "", false - } - return appstate.WAPatchName(name), true - }) -} - -var cmdResetAppState = &commands.FullHandler{ - Func: fnResetAppState, - Name: "reset-appstate", - Help: commands.HelpMeta{ - Section: commands.HelpSectionAdmin, - Description: "Ask your phone to reset broken app state. This logs out all linked devices.", - Args: "", - }, - 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 `\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", diff --git a/pkg/connector/config.go b/pkg/connector/config.go index 24874a26..e51c80a5 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -52,8 +52,6 @@ type Config struct { DirectMediaAutoRequest bool `yaml:"direct_media_auto_request"` InitialAutoReconnect bool `yaml:"initial_auto_reconnect"` UseWhatsAppRetryStore bool `yaml:"use_whatsapp_retry_store"` - MaxMemberSync int `yaml:"max_member_sync"` - LazyAvatars bool `yaml:"lazy_avatars"` AnimatedSticker msgconv.AnimatedStickerConfig `yaml:"animated_sticker"` @@ -131,8 +129,6 @@ func upgradeConfig(helper up.Helper) { helper.Copy(up.Bool, "direct_media_auto_request") helper.Copy(up.Bool, "initial_auto_reconnect") 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.Int, "animated_sticker", "args", "width") @@ -209,7 +205,6 @@ func (wa *WhatsAppConnector) GetConfig() (string, any, up.Upgrader) { {"proxy"}, {"displayname_template"}, {"call_start_notices"}, - {"animated_sticker"}, {"history_sync"}, }, Base: ExampleConfig, diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 80b9d95e..e262c82f 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -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, cmdResetAppState, cmdInviteLink, cmdResolveLink, cmdJoin, + cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin, ) wa.mediaEditCache = make(MediaEditCache) wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]() @@ -142,9 +142,6 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) { } 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) if err != nil { return bridgev2.DBUpgradeError{Err: err, Section: "whatsmeow"} @@ -160,25 +157,7 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error { return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"} } - err = wa.migrateToLIDDMs(ctx) - 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 + return wa.migrateToLIDDMs(ctx) } func (wa *WhatsAppConnector) Stop() { @@ -243,16 +222,7 @@ func (wa *WhatsAppConnector) GenerateTransactionID(_ id.UserID, _ id.RoomID, _ e } func (wa *WhatsAppConnector) ResetHTTPTransport() { - 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") - } - } + // No-op for now, whatsmeow doesn't use the shared transport config yet } func (wa *WhatsAppConnector) ResetNetworkConnections() { diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index 03ef3984..8a7afd9f 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -30,11 +30,9 @@ import ( "github.com/rs/zerolog" "go.mau.fi/util/exsync" - "go.mau.fi/util/jsontime" "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" @@ -86,15 +84,13 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars if waClient.Client == nil { 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) if err != nil { return nil, fmt.Errorf("failed to get avatar cache entry: %w", err) } - if cachedInfo.IsGone() { + if cachedInfo != nil && cachedInfo.Gone { return nil, mautrix.MNotFound.WithMessage("Avatar is no longer available (cached response)") - } else if cachedInfo.Expired() { + } else if cachedInfo == nil || cachedInfo.Expiry.Time.Before(time.Now().Add(5*time.Minute)) { zerolog.Ctx(ctx).Debug(). Str("avatar_id", parsedID.Avatar.AvatarID). Msg("Refreshing avatar URL from WhatsApp servers") @@ -103,7 +99,7 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars }) if errors.Is(err, whatsmeow.ErrProfilePictureNotSet) || errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) || - (err == nil && (avatar == nil || (avatar.ID != parsedID.Avatar.AvatarID && !parsedID.Avatar.IsRandom()))) { + (err == nil && (avatar == nil || avatar.ID != parsedID.Avatar.AvatarID)) { zerolog.Ctx(ctx).Debug(). Err(err). Stringer("target_jid", parsedID.Avatar.TargetJID). @@ -111,14 +107,9 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars Str("wanted_avatar_id", parsedID.Avatar.AvatarID). Str("got_avatar_id", ptr.Val(avatar).ID). 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{ EntityJID: parsedID.Avatar.TargetJID, AvatarID: parsedID.Avatar.AvatarID, - Expiry: goneExpiry, Gone: true, }) if err != nil { @@ -137,15 +128,6 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars Str("avatar_id", avatar.ID). 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{ Callback: func(w *os.File) (*mediaproxy.FileMeta, error) { @@ -164,19 +146,7 @@ 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) - 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 { + if err != nil { return nil, err } else if sticker == nil { return nil, mautrix.MNotFound.WithMessage("Sticker not found in pack") @@ -186,9 +156,6 @@ func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, par func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) { 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 { @@ -231,16 +198,6 @@ 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, @@ -298,11 +255,10 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse( type directMediaRetry struct { sync.Mutex - resultURL string - wait *exsync.Event - requested bool - resultType waMmsRetry.MediaRetryNotification_ResultType - decryptFail bool + resultURL string + wait *exsync.Event + requested bool + resultType waMmsRetry.MediaRetryNotification_ResultType } func (wa *WhatsAppClient) getDirectMediaRetryState(msgID networkid.MessageID, create bool) *directMediaRetry { @@ -329,9 +285,6 @@ func (wa *WhatsAppClient) requestAndWaitDirectMedia(ctx context.Context, rawMsgI keys.DirectPath = state.resultURL 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 { case waMmsRetry.MediaRetryNotification_NOT_FOUND: return mautrix.MNotFound.WithMessage("This media was not found on your phone.") @@ -385,9 +338,6 @@ func (wa *WhatsAppClient) receiveDirectMediaRetry(ctx context.Context, msg *data retryData, err := whatsmeow.DecryptMediaRetryNotification(retry, keys.Key) if err != nil { log.Warn().Err(err).Msg("Failed to decrypt media retry notification") - if state != nil { - state.decryptFail = true - } return } if state != nil { diff --git a/pkg/connector/errors.go b/pkg/connector/errors.go deleted file mode 100644 index 38433d6c..00000000 --- a/pkg/connector/errors.go +++ /dev/null @@ -1,156 +0,0 @@ -// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2026 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -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) -} diff --git a/pkg/connector/events.go b/pkg/connector/events.go index 7f906e89..1f4e118b 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -136,9 +136,6 @@ func (evt *WAMessageEvent) AddLogContext(c zerolog.Context) zerolog.Context { if targetMsg := evt.GetTargetMessage(); targetMsg != "" { c = c.Str("target_message_id", string(targetMsg)) } - if evt.parsedMessageType == "edit" { - c = c.Str("edit_inner_message_type", getMessageType(evt.Message.GetProtocolMessage().GetEditedMessage())) - } return evt.MessageInfoWrapper.AddLogContext(c).Str("parsed_message_type", evt.parsedMessageType) } @@ -192,49 +189,18 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por cacheMessage = evt.GetID() } else { editedMsg = evt.Message.GetProtocolMessage().GetEditedMessage() - if editedMsg.GetLottieStickerMessage().GetMessage() != nil { - editedMsg = editedMsg.GetLottieStickerMessage().GetMessage() - } - if editedMsg.GetDocumentWithCaptionMessage().GetMessage() != nil { - editedMsg = editedMsg.GetDocumentWithCaptionMessage().GetMessage() - } + previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, targetMessage) meta := existing[0].Metadata.(*waid.MessageMetadata) if slices.Contains(meta.Edits, evt.Info.ID) { return nil, fmt.Errorf("%w: edit already handled", bridgev2.ErrIgnoringRemoteEvent) } meta.Edits = append(meta.Edits, evt.Info.ID) - previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, targetMessage) - if previouslyConvertedPart == nil && needsPreviousEditPart(editedMsg) { - editedEvent, err := evt.wa.Main.Bridge.Bot.GetEvent(ctx, portal.MXID, existing[0].MXID) - if err != nil { - zerolog.Ctx(ctx).Err(err).Msg("Failed to fetch existing event for edit") - } else if editedEvent != nil && editedEvent.Content.AsMessage().MsgType != "" { - zerolog.Ctx(ctx).Debug().Msg("Fetched existing edit from server for caption edit") - copiedExtra := make(map[string]any) - for _, key := range []string{"info", msgconv.FailedMediaField} { - val, ok := editedEvent.Content.Raw[key] - if ok { - copiedExtra[key] = val - } - } - previouslyConvertedPart = &bridgev2.ConvertedMessagePart{ - ID: existing[0].PartID, - Type: editedEvent.Type, - Content: editedEvent.Content.AsMessage(), - Extra: copiedExtra, - DBMetadata: meta, - } - } else { - zerolog.Ctx(ctx).Debug().Msg("Didn't find existing edit on server for caption edit") - } - } } ctx = context.WithValue(ctx, msgconv.ContextKeyEditTargetID, evt.Message.GetProtocolMessage().GetKey().GetID()) cm := evt.wa.Main.MsgConv.ToMatrix( ctx, portal, evt.wa.Client, intent, editedMsg, evt.MsgEvent.RawMessage, &evt.Info, evt.isViewOnce(), false, previouslyConvertedPart, ) - evt.addMuseProfile(portal, cm) if evt.isUndecryptableUpsertSubEvent && isFailedMedia(cm) { evt.postHandle = func() { evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), cm, false) @@ -315,30 +281,11 @@ func (evt *WAMessageEvent) HandleExisting(ctx context.Context, portal *bridgev2. return bridgev2.UpsertResult{}, nil } -func (evt *WAMessageEvent) addMuseProfile(portal *bridgev2.Portal, converted *bridgev2.ConvertedMessage) { - if evt.Info.Chat != types.MuseJID || evt.Info.IsFromMe || !portal.NameIsCustom { - return - } - profile := &event.BeeperPerMessageProfile{ - ID: string(portal.MXID), - Displayname: portal.Name, - } - if portal.AvatarMXC != "" { - profile.AvatarURL = &portal.AvatarMXC - } - for _, part := range converted.Parts { - if part.Content != nil { - part.Content.BeeperPerMessageProfile = profile - } - } -} - func (evt *WAMessageEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) { evt.wa.EnqueuePortalResync(portal, false) converted := evt.wa.Main.MsgConv.ToMatrix( ctx, portal, evt.wa.Client, intent, evt.Message, evt.MsgEvent.RawMessage, &evt.Info, evt.isViewOnce(), false, nil, ) - evt.addMuseProfile(portal, converted) if isFailedMedia(converted) { evt.postHandle = func() { evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), converted, false) @@ -368,10 +315,7 @@ func (evt *WANowDecryptableMessage) GetTargetMessage() networkid.MessageID { } func (evt *WANowDecryptableMessage) AddLogContext(c zerolog.Context) zerolog.Context { - if evt.editParts != nil { - return c.Bool("from_upsert", true) - } - return evt.WAMessageEvent.AddLogContext(c.Bool("from_upsert", false)) + return c } func (evt *WANowDecryptableMessage) GetType() bridgev2.RemoteEventType { @@ -469,14 +413,24 @@ func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context { } func (evt *WAMediaRetry) getRealSender() types.JID { - return evt.wa.getRealSender(evt.ChatID, evt.SenderID, evt.FromMe) + 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 } func (evt *WAMediaRetry) GetSender() bridgev2.EventSender { realSender := pickLID(evt.getRealSender(), evt.senderLID) - sender := evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender) - sender.ForceEditOrigSender = true - return sender + return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender) } func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID { @@ -541,7 +495,6 @@ 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) diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index 905e6d61..564f25eb 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -69,12 +69,6 @@ initial_auto_reconnect: true # 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. 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. animated_sticker: diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 84198506..c55f523c 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -1,19 +1,3 @@ -// 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 . - package connector import ( @@ -35,7 +19,6 @@ 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" @@ -63,13 +46,12 @@ 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, wrapConversionError(fmt.Errorf("failed to convert poll start: %w", err)) + return nil, fmt.Errorf("failed to convert poll vote: %w", err) } defer wa.mcTrack(msg, time.Now(), &retErr) resp, err := wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil) @@ -89,7 +71,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, wrapConversionError(fmt.Errorf("failed to convert poll vote: %w", err)) + return nil, fmt.Errorf("failed to convert poll vote: %w", err) } defer wa.mcTrack(msg, time.Now(), &retErr) return wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil) @@ -98,17 +80,14 @@ 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, wrapConversionError(fmt.Errorf("failed to convert message: %w", err)) + return nil, fmt.Errorf("failed to convert message: %w", err) } defer wa.mcTrack(msg, time.Now(), &retErr) return wa.handleConvertedMatrixMessage(ctx, msg, waMsg, req) } -var ErrBroadcastSendDisabled = bridgev2.WrapErrorInStatus(errors.New("sending status messages is disabled")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) -var 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) +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) func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage, waMsg *waE2E.Message, req *whatsmeow.SendRequestExtra) (*bridgev2.MatrixMessageResponse, error) { if req == nil { @@ -119,7 +98,6 @@ 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 { @@ -138,10 +116,7 @@ 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 { - if errors.Is(err, whatsmeow.ErrNoPermissionInGroup) { - return nil, ErrNotCommunityAdmin - } - return nil, wa.wrapSendError(err) + return nil, err } var pickedMessageID networkid.MessageID if resp.Sender == wa.GetLID() { @@ -206,7 +181,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, wrapWhatsAppError(fmt.Errorf("failed to encrypt reaction: %w", err)) + return nil, fmt.Errorf("failed to encrypt reaction: %w", err) } reactionMsg.ReactionMessage = nil req.Meta = &types.MsgMetaInfo{ @@ -220,7 +195,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev Metadata: &waid.ReactionMetadata{ SenderDeviceID: wa.JID.Device, }, - }, wa.wrapSendError(err) + }, err } func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) (retErr error) { @@ -250,7 +225,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 wa.wrapSendError(err) + return err } func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) (retErr error) { @@ -275,7 +250,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 wrapConversionError(fmt.Errorf("failed to convert message: %w", err)) + return fmt.Errorf("failed to convert message: %w", err) } defer wa.mcTrack(edit, time.Now(), &retErr) @@ -290,7 +265,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M ID: editID, }) log.Trace().Any("response", resp).Msg("WhatsApp edit response") - return wa.wrapSendError(err) + return err } func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) (retErr error) { @@ -315,11 +290,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 wa.wrapSendError(err) + return err } func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) { - if receipt.Receipt.IsThread() || !receipt.ReadUpTo.After(receipt.LastRead) { + if !receipt.ReadUpTo.After(receipt.LastRead) { return nil } if receipt.LastRead.IsZero() { @@ -364,7 +339,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt * log.Err(err).Strs("ids", ids).Msg("Failed to mark messages as read") } } - return wrapWhatsAppError(err) + return err } func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) (retErr error) { @@ -395,7 +370,7 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2. zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set presence on typing") } } - return wrapWhatsAppError(wa.Client.SendChatPresence(ctx, portalJID, chatPresence, mediaPresence)) + return wa.Client.SendChatPresence(ctx, portalJID, chatPresence, mediaPresence) } var errUnsupportedDisappearingTimer = bridgev2.WrapErrorInStatus(errors.New("invalid value for disappearing timer")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true) @@ -416,7 +391,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, wrapWhatsAppError(err) + return false, err } msg.Portal.Metadata.(*waid.PortalMetadata).DisappearingTimerSetAt = settingTS.Unix() msg.Portal.Disappear = database.DisappearingSetting{ @@ -446,12 +421,6 @@ 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) @@ -482,11 +451,9 @@ 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, wrapWhatsAppError(err) + return nil, 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) } @@ -505,14 +472,12 @@ 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, wrapWhatsAppError(err) + return false, err } msg.Portal.Name = msg.Content.Name @@ -536,7 +501,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, wrapWhatsAppError(err) + return false, err } msg.Portal.Topic = msg.Content.Topic @@ -572,7 +537,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, wrapWhatsAppError(err) + return false, err } msg.Portal.AvatarMXC = msg.Content.URL @@ -601,7 +566,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("%w: avatar is too large for re-encoding", bridgev2.ErrMediaTooLarge) + return nil, fmt.Errorf("avatar is too large for re-encoding") } img, _, err := image.Decode(bytes.NewReader(data)) @@ -707,10 +672,7 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg if err != nil { return err } - 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 { + 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) @@ -718,7 +680,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 wrapWhatsAppError(fmt.Errorf("failed to leave group before deleting chat: %w", err)) + return fmt.Errorf("failed to leave group before deleting chat: %w", err) } } } @@ -727,37 +689,5 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg return err } defer wa.mcTrack(msg, time.Now(), &retErr) - 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) + return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true)) } diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index e6174b92..f8cee2ad 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -24,11 +24,9 @@ import ( "time" "github.com/rs/zerolog" - "go.mau.fi/util/exslices" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/appstate" - "go.mau.fi/whatsmeow/proto/waCommon" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" @@ -82,8 +80,6 @@ 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: @@ -176,12 +172,6 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { case *events.Connected: log.Debug().Msg("Connected to WhatsApp socket") wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) - if wa.offlineSyncWaiter.Load() == nil { - go func() { - wa.resyncWASARootSecrets(ctx) - wa.requestMuseProfile(ctx) - }() - } if len(wa.GetStore().PushName) > 0 { go func() { err := wa.updatePresence(ctx, types.PresenceUnavailable) @@ -212,7 +202,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) wa.notifyOfflineSyncWaiter(nil) case *events.LoggedOut: - wa.handleWALogout(ctx, evt.Reason, evt.OnConnect) + wa.handleWALogout(evt.Reason, evt.OnConnect) wa.notifyOfflineSyncWaiter(fmt.Errorf("logged out: %s", evt.Reason)) case *events.Disconnected: // Don't send the normal transient disconnect state if we're already in a different transient disconnect state. @@ -237,11 +227,7 @@ 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, - BlockAutoReconnect: true, - }) + wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAStreamReplaced}) wa.notifyOfflineSyncWaiter(fmt.Errorf("stream replaced")) case *events.KeepAliveTimeout: wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WAKeepaliveTimeout}) @@ -257,17 +243,12 @@ 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, - BlockAutoReconnect: true, - }) + wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAClientOutdated}) 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())) @@ -301,9 +282,7 @@ func (wa *WhatsAppClient) ensureAltJIDs(ctx context.Context, info *types.Message func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) { success = true - if (evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast) || - (evt.Info.Chat.Server == types.NewsletterServer && wa.disableNewsletter) || - (evt.Info.Chat.Server == types.BroadcastServer && evt.Info.IsFromMe) { + if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast { return } if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { @@ -351,21 +330,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa wa.Main.Bridge.Config.Backfill.Enabled { wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification) } - if protocol := evt.Message.GetProtocolMessage(); evt.Info.Chat == types.MuseJID && protocol.GetType() == waE2E.ProtocolMessage_AI_METADATA_OPERATION { - if sync := protocol.GetAiMetadataOperation().GetHatchMetadataSync(); sync != nil { - return wa.handleMuseMetadata(ctx, sync.GetData()) - } - return - } - 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") + if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") { return } @@ -373,16 +338,18 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation() 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 + protocolMsg := evt.Message.GetProtocolMessage() + if protocolMsg.GetType() != waE2E.ProtocolMessage_MESSAGE_EDIT || protocolMsg.GetKey() == nil { + 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 } - dontRenderEdited = shouldHideEdit wa.UserLogin.Log.Debug(). Str("message_id", evt.Info.ID). Str("parent_id", parentKey.GetID()). @@ -402,7 +369,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa return } - wrappedEvt := &WAMessageEvent{ + res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{ MessageInfoWrapper: &MessageInfoWrapper{ Info: evt.Info, wa: wa, @@ -412,40 +379,8 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa parsedMessageType: parsedMessageType, dontRenderEdited: dontRenderEdited, - } - if evt.UnavailableRequestID != "" { - wa.UserLogin.Log.Debug(). - Str("message_id", evt.Info.ID). - Str("unavailable_request_id", evt.UnavailableRequestID). - Msg("Received placeholder resend response") - wa.trackUndecryptableResolved(evt) - wrappedEvt.isUndecryptableUpsertSubEvent = true - // Dispatch directly as edit instead of the usual upsert, so that the message doesn't - // get re-bridged if it was deleted before the resend response was received. - return wa.UserLogin.QueueRemoteEvent(&WANowDecryptableMessage{WAMessageEvent: wrappedEvt}).Success - } - - return wa.UserLogin.QueueRemoteEvent(wrappedEvt).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 + }) + return res.Success } func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool { @@ -527,12 +462,6 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei messageSender := wa.GetLID() if !evt.MessageSender.IsEmpty() { messageSender = evt.MessageSender - } 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 { @@ -547,15 +476,10 @@ 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() { senderLID = evt.SenderAlt - } else if evt.Chat.Server == types.NewsletterServer && evt.Type == types.ReceiptTypeReadSelf { - senderLID = wa.GetLID() } res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{ EventMeta: simplevent.EventMeta{ @@ -563,11 +487,6 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei PortalKey: wa.makeWAPortalKey(evt.Chat), Sender: wa.makeEventSender(ctx, senderLID), Timestamp: evt.Timestamp, - LogContext: func(c zerolog.Context) zerolog.Context { - return c. - Strs("targets", exslices.CastToString[string](targets)). - Stringer("receipt_sender", senderLID) - }, }, Targets: targets, }) @@ -606,7 +525,7 @@ func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events. }) } -func (wa *WhatsAppClient) handleWALogout(ctx context.Context, reason events.ConnectFailureReason, onConnect bool) { +func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onConnect bool) { errorCode := WAUnknownLogout if reason == events.ConnectFailureLoggedOut { errorCode = WALoggedOut @@ -618,10 +537,6 @@ func (wa *WhatsAppClient) handleWALogout(ctx context.Context, reason events.Conn wa.JID = types.EmptyJID wa.LID = types.EmptyJID 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{ StateEvent: status.StateBadCredentials, Error: errorCode, @@ -737,27 +652,6 @@ 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, @@ -766,7 +660,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.getRealSender(chatJID, wa.maybeConvertJIDToLID(ctx, evt.SenderJID), evt.IsFromMe) + senderJID := wa.maybeConvertJIDToLID(ctx, evt.SenderJID) return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventMessageRemove, @@ -807,13 +701,13 @@ func (wa *WhatsAppClient) syncGhost(jid types.JID, reason string, pictureID *str if pictureID != nil && *pictureID != "" && ghost.AvatarID == networkid.AvatarID(*pictureID) { return } - userInfo, err := wa.getUserInfo(ctx, jid, ptr.Val(pictureID), pictureID != nil) + userInfo, err := wa.getUserInfo(ctx, jid, pictureID != nil) if err != nil { log.Err(err).Msg("Failed to get user info") } else { ghost.UpdateInfo(ctx, userInfo) log.Debug().Msg("Synced ghost info") - wa.syncAltGhostWithInfo(ctx, jid, ghost) + wa.syncAltGhostWithInfo(ctx, jid, userInfo) } go wa.syncRemoteProfile(ctx, ghost) } @@ -873,6 +767,9 @@ func (wa *WhatsAppClient) handleWAGroupInfoChange(ctx context.Context, evt *even } func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.JoinedGroup) bool { + if wa.createDedup.Pop(evt.CreateKey) { + return true + } return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventChatResync, @@ -885,9 +782,6 @@ func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.J } func (wa *WhatsAppClient) handleWANewsletterJoin(ctx context.Context, evt *events.NewsletterJoin) bool { - if wa.disableNewsletter { - return true - } return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventChatResync, @@ -978,9 +872,7 @@ 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 { - if !evt.Recovery { - delete(wa.appStateFullSyncAttempted, evt.Name) - } + delete(wa.appStateFullSyncAttempted, evt.Name) delete(meta.AppStateRecoveryAttempted, evt.Name) err := wa.UserLogin.Save(ctx) if err != nil { @@ -988,12 +880,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt } else { log.Info(). Time("recovery_ts", ts). - Bool("recovery_evt", evt.Recovery). Msg("Unmarked app state recovery as attempted after successful full sync") - wa.UserLogin.TrackAnalytics("WhatsApp Appstate Recovery Success", map[string]any{ - "patch_name": evt.Name, - "from_recovery": evt.Recovery, - }) } } else if ts, exists = wa.appStateFullSyncAttempted[evt.Name]; exists { delete(wa.appStateFullSyncAttempted, evt.Name) @@ -1018,7 +905,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev return } if !evt.FullSync { - if !lastFullSync.IsZero() && time.Since(lastFullSync) < 48*time.Hour { + if !lastFullSync.IsZero() { log.Debug(). Err(evt.Error). Time("last_full_sync_attempt", lastFullSync). @@ -1050,9 +937,6 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev if err != nil { log.Err(err).Msg("Failed to save login metadata after marking app state recovery as attempted") } - wa.UserLogin.TrackAnalytics("WhatsApp Appstate Recovery Request", map[string]any{ - "patch_name": evt.Name, - }) go func() { resp, err := wa.Client.SendPeerMessage(ctx, whatsmeow.BuildAppStateRecoveryRequest(evt.Name)) if err != nil { @@ -1065,110 +949,3 @@ 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, - }, - }, - } -} diff --git a/pkg/connector/id.go b/pkg/connector/id.go index 43e949a6..c0aa06b0 100644 --- a/pkg/connector/id.go +++ b/pkg/connector/id.go @@ -82,18 +82,3 @@ 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 -} diff --git a/pkg/connector/lidmigrate.go b/pkg/connector/lidmigrate.go index 4295be63..cfab099b 100644 --- a/pkg/connector/lidmigrate.go +++ b/pkg/connector/lidmigrate.go @@ -18,7 +18,6 @@ package connector import ( "context" - "errors" "fmt" "time" @@ -26,11 +25,9 @@ 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" ) @@ -50,8 +47,6 @@ 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 @@ -63,8 +58,6 @@ 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()) } @@ -217,165 +210,3 @@ func (wa *WhatsAppConnector) migrateToLIDDMs(ctx context.Context) error { wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_migrated", "true") 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 - } - 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 -} - -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 -}) diff --git a/pkg/connector/login.go b/pkg/connector/login.go index ae57ac1a..5da419f6 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -6,14 +6,12 @@ import ( "errors" "fmt" "net/http" - "strings" "sync/atomic" "time" "github.com/rs/zerolog" "go.mau.fi/util/exsync" "go.mau.fi/util/jsontime" - "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" @@ -100,7 +98,6 @@ func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User, LoginComplete: exsync.NewEvent(), PasskeyRequest: exsync.NewEvent(), PasskeyConfirmation: exsync.NewEvent(), - ADVRotate: exsync.NewEvent(), Received515: exsync.NewEvent(), }, nil } @@ -127,8 +124,6 @@ type WALogin struct { PasskeyConfirmation *exsync.Event PasskeyConfirmationData *events.PairPasskeyConfirmation - ADVRotate *exsync.Event - Closed atomic.Bool EventHandlerID uint32 } @@ -137,22 +132,12 @@ var ( _ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil) _ bridgev2.LoginProcessUserInput = (*WALogin)(nil) _ bridgev2.LoginProcessWithOverride = (*WALogin)(nil) - _ bridgev2.LoginProcessWithParams = (*WALogin)(nil) _ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil) ) -const LoginConnectWait = 30 * time.Second -const LoginPairPhoneWait = 30 * time.Second +const LoginConnectWait = 15 * time.Second func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { - return wl.StartWithParams(ctx, bridgev2.LoginStartParams{}) -} - -func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogin) (*bridgev2.LoginStep, error) { - return wl.StartWithParams(ctx, bridgev2.LoginStartParams{Override: old}) -} - -func (wl *WALogin) StartWithParams(ctx context.Context, params bridgev2.LoginStartParams) (*bridgev2.LoginStep, error) { wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect) device := wl.Main.DeviceStore.NewDevice() wl.Client = whatsmeow.NewClient(device, waLog.Zerolog(wl.Log)) @@ -162,20 +147,8 @@ func (wl *WALogin) StartWithParams(ctx context.Context, params bridgev2.LoginSta if err := wl.Main.updateProxy(ctx, wl.Client, true); err != nil { return nil, err } - if params.WebSocketHTTP != nil { - wl.Client.SetPreLoginHTTPClient(&http.Client{Transport: params.WebSocketHTTP}) - } if wl.PhoneCode { - if params.Override != nil { - phoneNumber := fmt.Sprintf("+%s", params.Override.ID) - wl.Log.Debug(). - Str("phone_number", phoneNumber). - Msg("Auto-submitting phone number for relogin") - return wl.SubmitUserInput(ctx, map[string]string{ - "phone_number": phoneNumber, - }) - } return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeUserInput, StepID: LoginStepIDPhoneNumber, @@ -192,7 +165,7 @@ func (wl *WALogin) StartWithParams(ctx context.Context, params bridgev2.LoginSta err := wl.Client.Connect() if err != nil { wl.Log.Err(err).Msg("Failed to connect to WhatsApp for QR login") - return nil, wrapWhatsAppRespError(err) + return nil, err } ctx, cancel := context.WithTimeout(ctx, LoginConnectWait) defer cancel() @@ -205,22 +178,34 @@ func (wl *WALogin) StartWithParams(ctx context.Context, params bridgev2.LoginSta return makeQRStep(wl.QRs[0]), nil } +func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogin) (*bridgev2.LoginStep, error) { + step, err := wl.Start(ctx) + if err == nil && step != nil && old != nil && step.StepID == LoginStepIDPhoneNumber { + phoneNumber := fmt.Sprintf("+%s", old.ID) + wl.Log.Debug(). + Str("phone_number", phoneNumber). + Msg("Auto-submitting phone number for relogin") + return wl.SubmitUserInput(ctx, map[string]string{ + "phone_number": phoneNumber, + }) + } + return step, err +} + func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { + ctx, cancel := context.WithTimeout(ctx, LoginConnectWait) + defer cancel() err := wl.Client.Connect() if err != nil { wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login") - return nil, wrapWhatsAppRespError(err) + return nil, err } - connectCtx, cancelConnect := context.WithTimeout(ctx, LoginConnectWait) - err = wl.WaitForQRs.Wait(connectCtx) - cancelConnect() + err = wl.WaitForQRs.Wait(ctx) if err != nil { wl.Log.Warn().Err(err).Msg("Timed out waiting for connection") return nil, fmt.Errorf("failed to wait for connection: %w", err) } - pairCtx, cancelPair := context.WithTimeout(ctx, LoginPairPhoneWait) - defer cancelPair() - pairingCode, err := wl.Client.PairPhone(pairCtx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)") + pairingCode, err := wl.Client.PairPhone(ctx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)") if err != nil { wl.Log.Err(err).Msg("Failed to request phone code login") if errors.Is(err, whatsmeow.ErrPhoneNumberTooShort) { @@ -230,7 +215,7 @@ func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) } else if errors.Is(err, whatsmeow.ErrIQRateOverLimit) { return nil, ErrRateLimitedByWhatsApp } - return nil, wrapWhatsAppRespError(err) + return nil, err } wl.Log.Debug().Msg("Phone code login started") return &bridgev2.LoginStep{ @@ -282,13 +267,6 @@ func (wl *WALogin) handleEvent(rawEvt any) { wl.StartTime = time.Now() wl.WaitForQRs.Set() 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() - return case *events.QRScannedWithoutMultidevice: wl.Log.Error().Msg("QR code scanned without multidevice enabled") wl.LoginError = ErrLoginMultideviceNotEnabled @@ -324,7 +302,6 @@ func (wl *WALogin) handleEvent(rawEvt any) { wl.LoginError = ErrUnexpectedDisconnect case *events.ManualLoginReconnect: wl.Received515.Set() - return default: wl.Log.Warn().Type("event_type", evt).Msg("Got unexpected event") return @@ -352,7 +329,6 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { Int("current_index", currentIndex) if currentIndex > prevIndex { logEvt.Msg("Returning new QR immediately") - wl.ADVRotate.Clear() wl.PrevQRIndex.Store(int32(currentIndex)) return makeQRStep(wl.QRs[currentIndex]), nil } @@ -364,7 +340,6 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { wl.Cancel() return nil, ErrLoginTimeout } - wl.ADVRotate.Clear() wl.PrevQRIndex.Store(int32(nextIndex)) return makeQRStep(wl.QRs[nextIndex]), nil case <-ctx.Done(): @@ -372,15 +347,6 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { return nil, ctx.Err() case <-wl.PasskeyRequest.GetChan(): return wl.makePasskeyStep() - case <-wl.ADVRotate.GetChan(): - wl.ADVRotate.Clear() - if currentIndex < 0 || currentIndex >= len(wl.QRs) { - wl.Log.Debug().Msg("ADV secret was rotated, but no QRs are left to return") - wl.Cancel() - return nil, ErrLoginTimeout - } - wl.Log.Debug().Msg("ADV secret was rotated, returning current QR immediately") - return makeQRStep(wl.QRs[currentIndex]), nil case <-wl.LoginComplete.GetChan(): // continue } @@ -417,7 +383,6 @@ func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, er Timezone: wl.Timezone, HistorySyncPortalsNeedCreating: true, - WASAResynced: true, }, }, &bridgev2.NewLoginParams{ DeleteOnConflict: true, @@ -426,15 +391,8 @@ func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, er return nil, fmt.Errorf("failed to create user login: %w", err) } - c := ul.Client.(*WhatsAppClient) - c.isNewLogin = true - if c.connect(ul.Log.WithContext(wl.Main.Bridge.BackgroundCtx)) { - err = c.Client.PreKeysUploaded.WaitTimeoutCtx(ctx, 5*time.Second) - if err != nil { - wl.Log.Warn().Err(err).Msg("Prekey upload wait failed") - } - go c.syncGhost(c.JID.ToNonAD(), "login", ptr.Ptr("")) - } + ul.Client.(*WhatsAppClient).isNewLogin = true + ul.Client.Connect(ul.Log.WithContext(wl.Main.Bridge.BackgroundCtx)) return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeComplete, @@ -475,7 +433,7 @@ func (wl *WALogin) SubmitWebAuthnResponse(ctx context.Context, rawResp json.RawM if err != nil { wl.Log.Err(err).Msg("Failed to send WebAuthn response") wl.Cancel() - return nil, wrapWhatsAppRespError(err) + return nil, err } select { case <-ctx.Done(): @@ -494,7 +452,7 @@ func (wl *WALogin) SubmitWebAuthnResponse(ctx context.Context, rawResp json.RawM if err != nil { wl.Log.Err(err).Msg("Failed to send WebAuthn confirmation") wl.Cancel() - return nil, wrapWhatsAppRespError(err) + return nil, err } case <-wl.LoginComplete.GetChan(): } diff --git a/pkg/connector/mediarequest.go b/pkg/connector/mediarequest.go index 6a30cf6f..38061967 100644 --- a/pkg/connector/mediarequest.go +++ b/pkg/connector/mediarequest.go @@ -88,7 +88,6 @@ func (wa *WhatsAppClient) mediaRequestLoop(ctx context.Context) { return case <-time.After(startIn): } - wa.sendMediaRequests(ctx) ticker := time.NewTicker(24 * time.Hour) defer ticker.Stop() for { diff --git a/pkg/connector/muse.go b/pkg/connector/muse.go deleted file mode 100644 index 4a589ce7..00000000 --- a/pkg/connector/muse.go +++ /dev/null @@ -1,195 +0,0 @@ -package connector - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "runtime/debug" - "time" - - "github.com/google/uuid" - "github.com/rs/zerolog" - "go.mau.fi/util/exerrors" - "go.mau.fi/whatsmeow" - "go.mau.fi/whatsmeow/appstate" - "go.mau.fi/whatsmeow/proto/waAICommon" - "go.mau.fi/whatsmeow/proto/waE2E" - "go.mau.fi/whatsmeow/types" - "google.golang.org/protobuf/proto" - "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/bridgev2/database" - "maunium.net/go/mautrix/bridgev2/networkid" - "maunium.net/go/mautrix/bridgev2/simplevent" - - "go.mau.fi/mautrix-whatsapp/pkg/waid" -) - -type museImage struct { - DirectPath string `json:"direct_path"` - EncHash []byte `json:"file_enc_sha256_b64"` - Hash []byte `json:"file_sha256_b64"` - Key []byte `json:"media_key_b64"` - Length int `json:"file_length"` -} - -func (wa *WhatsAppClient) resyncWASARootSecrets(ctx context.Context) { - defer func() { - v := recover() - if v != nil { - zerolog.Ctx(ctx).Err(exerrors.RecoverToError(v)). - Bytes("stack", debug.Stack()). - Msg("Error resyncing WASA root secrets") - } - }() - wa.wasaResyncLock.Lock() - defer wa.wasaResyncLock.Unlock() - if wa.offlineSyncWaiter.Load() != nil || !wa.Client.IsConnected() { - return - } - meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata) - if meta.WASAResynced { - return - } - log := zerolog.Ctx(ctx) - log.Info().Msg("Resyncing WASA root secrets for existing login") - success := true - if err := wa.Client.FetchAppState(ctx, appstate.WAPatchRegularHigh, true, false); err != nil { - log.Err(err).Msg("Failed to resync WASA root secrets") - // Mark the sync as completed on errors too, because some get permanently stuck (mismatching LTHash, - // expired patch blobs), and a failed full sync already requests app state recovery from the phone. - if ctx.Err() != nil { - return - } - success = false - } - meta.WASAResynced = true - if err := wa.UserLogin.Save(ctx); err != nil { - meta.WASAResynced = false - log.Err(err).Msg("Failed to save WASA resync completion") - return - } - if success { - log.Info().Msg("Completed WASA root secret resync") - } else { - log.Info().Msg("Marked WASA root resync as done despite errors") - } -} - -func (wa *WhatsAppClient) requestMuseProfile(ctx context.Context) { - defer func() { - v := recover() - if v != nil { - zerolog.Ctx(ctx).Err(exerrors.RecoverToError(v)). - Bytes("stack", debug.Stack()). - Msg("Error requesting Muse profile") - } - }() - if !wa.museProfileLock.TryLock() { - return - } - defer wa.museProfileLock.Unlock() - if wa.offlineSyncWaiter.Load() != nil || !wa.Client.IsConnected() || time.Since(wa.lastMuseProfileRequest) < 15*time.Minute { - return - } - portal, err := wa.Main.Bridge.DB.Portal.GetByKey(ctx, wa.makeWAPortalKey(types.MuseJID)) - if err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to check Muse profile") - return - } else if portal != nil && portal.NameIsCustom && portal.Name != "" && portal.NameSet && museAvatarReady(portal) { - return - } - rootID, err := wa.GetStore().ChatSettings.GetWASARootSecretID(ctx, types.MuseJID) - if err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to check Muse root secret") - return - } else if rootID == "" { - return - } - zerolog.Ctx(ctx).Info().Msg("Sending request to sync Muse profile") - wa.lastMuseProfileRequest = time.Now() - _, err = wa.Client.SendMessage(ctx, types.MuseJID, &waE2E.Message{ - ProtocolMessage: &waE2E.ProtocolMessage{ - Type: waE2E.ProtocolMessage_AI_METADATA_OPERATION.Enum(), - AiMetadataOperation: &waAICommon.AIMetadataOperation{ - HatchMetadataSync: &waAICommon.HatchMetadataSync{ - Data: []byte(`{"version":1,"type":"req","payload":{"method":"channel.bootstrap","params":{"sections":["agent.status","identity.updated","hitl.snapshot"]}}}`), - TimestampMS: proto.Int64(time.Now().UnixMilli()), - RequestID: proto.String(uuid.NewString()), - }, - }, - }, - }) - if err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to request Muse profile") - } -} - -func museAvatarReady(portal *database.Portal) bool { - expectedHash, err := base64.StdEncoding.DecodeString(string(portal.AvatarID)) - return err != nil || len(expectedHash) != 32 || - (portal.AvatarMXC != "" && portal.AvatarHash == [32]byte(expectedHash)) -} - -func (wa *WhatsAppClient) handleMuseMetadata(ctx context.Context, data []byte) bool { - var msg struct { - Type string `json:"type"` - Payload struct { - Event string `json:"event"` - Payload struct { - Name string `json:"name"` - Avatar struct { - Image museImage `json:"secure_image"` - } `json:"avatar"` - } `json:"payload"` - } `json:"payload"` - } - if err := json.Unmarshal(data, &msg); err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to decode Muse metadata") - return true - } - if msg.Type != "event" || msg.Payload.Event != "identity.updated" || msg.Payload.Payload.Name == "" { - return true - } - name := msg.Payload.Payload.Name - avatar := wa.museAvatar(msg.Payload.Payload.Avatar.Image) - return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{ - EventMeta: simplevent.EventMeta{ - Type: bridgev2.RemoteEventChatInfoChange, - PortalKey: wa.makeWAPortalKey(types.MuseJID), - CreatePortal: true, - PreHandleFunc: func(_ context.Context, portal *bridgev2.Portal) { - if avatar != nil && !museAvatarReady(portal.Portal) { - portal.AvatarSet = false - } - }, - }, - ChatInfoChange: &bridgev2.ChatInfoChange{ - ChatInfo: &bridgev2.ChatInfo{ - Name: &name, - Avatar: avatar, - ExcludeChangesFromTimeline: true, - }, - }, - }).Success -} - -func (wa *WhatsAppClient) museAvatar(image museImage) *bridgev2.Avatar { - if image.DirectPath == "" || image.Length <= 0 || image.Length > 5<<20 { - return nil - } - if len(image.EncHash) != 32 || len(image.Hash) != 32 || len(image.Key) != 32 { - return nil - } - return &bridgev2.Avatar{ - ID: networkid.AvatarID(base64.StdEncoding.EncodeToString(image.Hash)), - Get: func(ctx context.Context) ([]byte, error) { - data, err := wa.Client.DownloadMediaWithPath(ctx, image.DirectPath, image.EncHash, image.Hash, image.Key, whatsmeow.MediaImage, "image", false) - if err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to download Muse avatar") - return nil, fmt.Errorf("failed to download Muse avatar: %w", err) - } - return data, nil - }, - } -} diff --git a/pkg/connector/proxy.go b/pkg/connector/proxy.go index 8bd6d658..5ae103db 100644 --- a/pkg/connector/proxy.go +++ b/pkg/connector/proxy.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "net/url" - "time" "github.com/rs/zerolog" "go.mau.fi/whatsmeow" @@ -15,13 +14,11 @@ import ( // TODO move proxy stuff to mautrix-go -var getProxyClient = &http.Client{Timeout: 30 * time.Second} - type respGetProxy struct { ProxyURL string `json:"proxy_url"` } -func (wa *WhatsAppConnector) getProxy(ctx context.Context, reason string) (string, error) { +func (wa *WhatsAppConnector) getProxy(reason string) (string, error) { if wa.Config.GetProxyURL == "" { return wa.Config.Proxy, nil } @@ -32,17 +29,15 @@ func (wa *WhatsAppConnector) getProxy(ctx context.Context, reason string) (strin q := parsed.Query() q.Set("reason", reason) parsed.RawQuery = q.Encode() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil) + req, err := http.NewRequest(http.MethodGet, parsed.String(), nil) if err != nil { return "", fmt.Errorf("failed to prepare request: %w", err) } req.Header.Set("User-Agent", mautrix.DefaultUserAgent) - resp, err := getProxyClient.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { return "", fmt.Errorf("failed to send request: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode >= 300 || resp.StatusCode < 200 { + } else if resp.StatusCode >= 300 || resp.StatusCode < 200 { return "", fmt.Errorf("unexpected status code %d", resp.StatusCode) } var respData respGetProxy @@ -54,38 +49,23 @@ func (wa *WhatsAppConnector) getProxy(ctx context.Context, reason string) (strin } func (wa *WhatsAppConnector) updateProxy(ctx context.Context, client *whatsmeow.Client, isLogin bool) error { - var proxy string - if !wa.Config.ProxyOnlyLogin || isLogin { - reason := "connect" - if isLogin { - reason = "login" - } - var err error - proxy, err = wa.getProxy(ctx, reason) - if err != nil { - return fmt.Errorf("failed to get proxy address: %w", err) - } + if wa.Config.ProxyOnlyLogin && !isLogin { + return nil } - settings := wa.Bridge.GetHTTPClientSettings().WithGlobalTimeout(0) - if settings.HTTPProxy == nil { - settings.HTTPProxy = http.ProxyFromEnvironment + reason := "connect" + if isLogin { + reason = "login" } - proxySettings := settings - if proxy != "" { - var err error - proxySettings, err = settings.WithProxy(proxy) - if err != nil { - return fmt.Errorf("failed to set proxy address: %w", err) - } - } - client.SetPreLoginHTTPClient(proxySettings.Compile()) - if wa.Config.ProxyOnlyLogin { - proxySettings = settings - } - client.SetWebsocketHTTPClient(proxySettings.Compile()) - client.SetMediaHTTPClient(proxySettings.WithResponseHeaderTimeout(0).Compile()) - if proxy != "" { - zerolog.Ctx(ctx).Debug().Msg("Enabled proxy") + if proxy, err := wa.getProxy(reason); err != nil { + return fmt.Errorf("failed to get proxy address: %w", err) + } else if proxy == "" { + return nil + } else if err = client.SetProxyAddress(proxy, whatsmeow.SetProxyOptions{ + OnlyLogin: wa.Config.ProxyOnlyLogin, + NoMedia: wa.Config.ProxyOnlyLogin, + }); err != nil { + return fmt.Errorf("failed to set proxy address: %w", err) } + zerolog.Ctx(ctx).Debug().Msg("Enabled proxy") return nil } diff --git a/pkg/connector/startchat.go b/pkg/connector/startchat.go index d4e5f8e8..f0d869b9 100644 --- a/pkg/connector/startchat.go +++ b/pkg/connector/startchat.go @@ -64,12 +64,12 @@ func looksEmaily(str string) bool { return false } -type isOnWhatsappCacheEntry struct { +type cacheEntry struct { jid types.JID ts time.Time } -var isOnWhatsappCache = exsync.NewMap[string, isOnWhatsappCacheEntry]() +var isOnWhatsappCache = exsync.NewMap[string, cacheEntry]() func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) (types.JID, error) { if strings.HasSuffix(number, "@"+types.BotServer) || strings.HasSuffix(number, "@"+types.HiddenUserServer) { @@ -88,13 +88,13 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) } else if entry, ok := isOnWhatsappCache.Get(number); ok && time.Since(entry.ts) < 4*time.Hour { return entry.jid, nil } else if resp, err := wa.Client.IsOnWhatsApp(ctx, []string{number}); err != nil { - return types.EmptyJID, wrapWhatsAppRespError(fmt.Errorf("failed to check if number is on WhatsApp: %w", err)) + return types.EmptyJID, fmt.Errorf("failed to check if number is on WhatsApp: %w", err) } else if len(resp) == 0 { return types.EmptyJID, fmt.Errorf("the server did not respond to the query") } 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) } else { - isOnWhatsappCache.Set(number, isOnWhatsappCacheEntry{resp[0].JID, time.Now()}) + isOnWhatsappCache.Set(number, cacheEntry{resp[0].JID, time.Now()}) return resp[0].JID, nil } } @@ -128,7 +128,7 @@ func (wa *WhatsAppClient) startChatPNToLID(ctx context.Context, jid types.JID) ( } else if lid.IsEmpty() { resp, err := wa.Client.GetUserInfo(ctx, []types.JID{jid}) if err != nil { - return jid, wrapWhatsAppRespError(fmt.Errorf("failed to get user info for phone number: %w", err)) + return jid, fmt.Errorf("failed to get user info for phone number: %w", err) } else if info, ok := resp[jid]; !ok { return jid, fmt.Errorf("server didn't return user info for phone number") } else if info.LID.IsEmpty() { @@ -176,16 +176,11 @@ func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier stri if err != nil { 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{ - Ghost: ghost, - UserID: waid.MakeUserID(jid), - UserInfo: userInfo, - Chat: wa.makeCreateChatResponse(ctx, jid, origJID), + Ghost: ghost, + UserID: waid.MakeUserID(jid), + Chat: wa.makeCreateChatResponse(ctx, jid, origJID), }, nil } @@ -243,7 +238,7 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl resp = append(resp, &bridgev2.ResolveIdentifierResponse{ Ghost: ghost, UserID: waid.MakeUserID(jid), - UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, "", false), + UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, false), Chat: chatResp, }) } @@ -251,9 +246,14 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl } func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) { + createKey := wa.Client.GenerateMessageID() + if params.RoomID != "" { + wa.createDedup.Add(createKey) + } req := whatsmeow.ReqCreateGroup{ Name: ptr.Val(params.Name).Name, Participants: make([]types.JID, len(params.Participants)), + CreateKey: createKey, } for i, participant := range params.Participants { jid := waid.ParseUserID(participant) @@ -292,7 +292,7 @@ func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.Grou } resp, err := wa.Client.CreateGroup(ctx, req) if err != nil { - return nil, wrapWhatsAppRespError(fmt.Errorf("failed to create group: %w", err)) + return nil, fmt.Errorf("failed to create group: %w", err) } failedParticipants := make(map[networkid.UserID]*bridgev2.CreateChatFailedParticipant) filteredParticipants := resp.Participants[:0] diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index 387971e9..e8279558 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -29,15 +29,12 @@ var ResyncLoopInterval = 4 * time.Hour var ResyncJitterSeconds = 3600 func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) { - jid := waid.ParseUserID(ghost.ID) - if jid.Server == types.BotServer { - return - } lastSync := ghost.Metadata.(*waid.GhostMetadata).LastSync.Time - if ghost.AvatarID != "" && lastSync.Add(ResyncMinInterval).After(time.Now()) { + if lastSync.Add(ResyncMinInterval).After(time.Now()) { return } wa.resyncQueueLock.Lock() + jid := waid.ParseUserID(ghost.ID) if _, exists := wa.resyncQueue[jid]; !exists { wa.resyncQueue[jid] = resyncQueueItem{ghost: ghost} nextResyncIn := time.Until(wa.nextResync).String() @@ -119,17 +116,14 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID var ghostJIDs []types.JID var ghosts []*bridgev2.Ghost var portals []*bridgev2.Portal - selectedGhosts := make(map[types.JID]*bridgev2.Ghost) for jid, item := range queue { var lastSync time.Time - unknownAvatar := false if item.ghost != nil { lastSync = item.ghost.Metadata.(*waid.GhostMetadata).LastSync.Time - unknownAvatar = item.ghost.AvatarID == "" } else if item.portal != nil { lastSync = item.portal.Metadata.(*waid.PortalMetadata).LastSync.Time } - if !unknownAvatar && lastSync.Add(ResyncMinInterval).After(time.Now()) { + if lastSync.Add(ResyncMinInterval).After(time.Now()) { log.Debug(). Stringer("jid", jid). Time("last_sync", lastSync). @@ -137,23 +131,12 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID continue } if item.ghost != nil { - selectedGhosts[jid] = item.ghost + ghosts = append(ghosts, item.ghost) + ghostJIDs = append(ghostJIDs, jid) } else if item.portal != nil { portals = append(portals, item.portal) } } - for jid, ghost := range selectedGhosts { - if jid.Server == types.DefaultUserServer { - lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) - if err != nil { - log.Warn().Err(err).Stringer("jid", jid).Msg("Failed to get LID for background sync") - } else if _, lidSelected := selectedGhosts[lid]; lidSelected { - continue - } - } - ghosts = append(ghosts, ghost) - ghostJIDs = append(ghostJIDs, jid) - } for _, portal := range portals { wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ EventMeta: simplevent.EventMeta{ @@ -182,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") continue } - userInfo, err := wa.getUserInfo(ctx, jid, info.PictureID, ghost.AvatarID == "" || (info.PictureID != "" && string(ghost.AvatarID) != info.PictureID)) + userInfo, err := wa.getUserInfo(ctx, jid, info.PictureID != "" && string(ghost.AvatarID) != info.PictureID) if err != nil { log.Err(err).Stringer("jid", jid).Msg("Failed to get user info for puppet in background sync") continue } ghost.UpdateInfo(ctx, userInfo) - wa.syncAltGhostWithInfo(ctx, jid, ghost) + wa.syncAltGhostWithInfo(ctx, jid, userInfo) } } @@ -198,18 +181,18 @@ func (wa *WhatsAppClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost return nil, nil } 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, avatarID string, fetchAvatar bool) (*bridgev2.UserInfo, error) { +func (wa *WhatsAppClient) getUserInfo(ctx context.Context, jid types.JID, fetchAvatar bool) (*bridgev2.UserInfo, error) { contact, err := wa.GetStore().Contacts.GetContact(ctx, jid) if err != nil { return nil, err } - return wa.contactToUserInfo(ctx, jid, contact, avatarID, fetchAvatar), nil + return wa.contactToUserInfo(ctx, jid, contact, fetchAvatar), nil } -func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, contact types.ContactInfo, avatarID string, fetchAvatar bool) *bridgev2.UserInfo { +func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, contact types.ContactInfo, getAvatar bool) *bridgev2.UserInfo { if jid == types.MetaAIJID && contact.PushName == jid.User { contact.PushName = "Meta AI" } else if jid == types.LegacyPSAJID || jid == types.PSAJID { @@ -287,9 +270,7 @@ func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, } else if phone != "" { ui.Identifiers = []string{fmt.Sprintf("tel:%s", phone)} } - if wa.Main.Config.LazyAvatars { - ui.ExtraUpdates = bridgev2.MergeExtraUpdaters(ui.ExtraUpdates, wa.makeLazyGhostAvatarUpdater(avatarID, fetchAvatar)) - } else if fetchAvatar { + if getAvatar { ui.ExtraUpdates = bridgev2.MergeExtraUpdaters(ui.ExtraUpdates, wa.fetchGhostAvatar) } return ui @@ -326,21 +307,6 @@ 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) { mxc, err := wa.Main.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeAvatarMediaID(jid, avatar.ID, wa.UserLogin.ID, community)) if err != nil { @@ -362,21 +328,6 @@ func (wa *WhatsAppClient) makeDirectMediaAvatar(ctx context.Context, jid types.J }, 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 { jid := waid.ParseUserID(ghost.ID) existingID := string(ghost.AvatarID) @@ -448,14 +399,14 @@ func (wa *WhatsAppClient) resyncContacts(forceAvatarSync, automatic bool) { } else if contact, err := contactStore.GetContact(ctx, jid); err != nil { log.Err(err).Stringer("jid", jid).Msg("Failed to get contact info") } else { - userInfo := wa.contactToUserInfo(ctx, jid, contact, "", forceAvatarSync || ghost.AvatarID == "") + userInfo := wa.contactToUserInfo(ctx, jid, contact, forceAvatarSync || ghost.AvatarID == "") ghost.UpdateInfo(ctx, userInfo) - wa.syncAltGhostWithInfo(ctx, jid, ghost) + wa.syncAltGhostWithInfo(ctx, jid, userInfo) } } } -func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JID, mainGhost *bridgev2.Ghost) { +func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JID, info *bridgev2.UserInfo) { log := zerolog.Ctx(ctx) var altJID types.JID var err error @@ -480,28 +431,10 @@ func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JI Msg("Failed to get ghost for alternate JID") return } - ghost.UpdateInfo(ctx, makeInfoFromGhost(mainGhost)) + ghost.UpdateInfo(ctx, info) log.Debug(). Stringer("jid", jid). Stringer("alternate_jid", altJID). Msg("Synced alternate ghost with info") go wa.syncRemoteProfile(ctx, ghost) } - -func makeInfoFromGhost(ghost *bridgev2.Ghost) *bridgev2.UserInfo { - info := &bridgev2.UserInfo{ - Identifiers: ghost.Identifiers, - Name: &ghost.Name, - IsBot: &ghost.IsBot, - ExtraProfile: ghost.ExtraProfile, - } - if ghost.AvatarID != "" && ghost.AvatarID != "unauthorized" { - info.Avatar = &bridgev2.Avatar{ - ID: ghost.AvatarID, - Remove: ghost.AvatarMXC == "", - MXC: ghost.AvatarMXC, - Hash: ghost.AvatarHash, - } - } - return info -} diff --git a/pkg/connector/wadb/avatarcache.go b/pkg/connector/wadb/avatarcache.go index 55fb4b97..8ea947f2 100644 --- a/pkg/connector/wadb/avatarcache.go +++ b/pkg/connector/wadb/avatarcache.go @@ -2,14 +2,10 @@ package wadb import ( "context" - "strings" - "time" "go.mau.fi/util/dbutil" "go.mau.fi/util/jsontime" "go.mau.fi/whatsmeow/types" - - "go.mau.fi/mautrix-whatsapp/pkg/waid" ) type AvatarCacheQuery struct { @@ -53,13 +49,3 @@ func (ace *AvatarCacheEntry) Scan(row dbutil.Scannable) (*AvatarCacheEntry, erro func (ace *AvatarCacheEntry) sqlVariables() []any { 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)) -} diff --git a/pkg/connector/wadb/message.go b/pkg/connector/wadb/message.go index 7a6c63d1..4b160026 100644 --- a/pkg/connector/wadb/message.go +++ b/pkg/connector/wadb/message.go @@ -96,12 +96,12 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo args := []any{mq.BridgeID, loginID, chatJID} argNum := 4 if startTime != nil { - whereClauses += fmt.Sprintf(" AND timestamp > $%d", argNum) + whereClauses += fmt.Sprintf(" AND timestamp >= $%d", argNum) args = append(args, startTime.Unix()) argNum++ } if endTime != nil { - whereClauses += fmt.Sprintf(" AND timestamp < $%d", argNum) + whereClauses += fmt.Sprintf(" AND timestamp <= $%d", argNum) args = append(args, endTime.Unix()) } diff --git a/pkg/connector/wadb/upgrades/00-latest-schema.sql b/pkg/connector/wadb/upgrades/00-latest-schema.sql index c4172d09..0f1fef94 100644 --- a/pkg/connector/wadb/upgrades/00-latest-schema.sql +++ b/pkg/connector/wadb/upgrades/00-latest-schema.sql @@ -1,4 +1,4 @@ --- v0 -> v11 (compatible with v3+): Latest revision +-- v0 -> v10 (compatible with v3+): Latest revision CREATE TABLE whatsapp_poll_option_id ( bridge_id TEXT NOT NULL, diff --git a/pkg/connector/wadb/upgrades/11-rearm-leftover-history-sync-backfill.sql b/pkg/connector/wadb/upgrades/11-rearm-leftover-history-sync-backfill.sql deleted file mode 100644 index 69e8b1e7..00000000 --- a/pkg/connector/wadb/upgrades/11-rearm-leftover-history-sync-backfill.sql +++ /dev/null @@ -1,10 +0,0 @@ --- v11 (compatible with v3+): Re-arm backfill for chats with leftover history sync messages -UPDATE backfill_task -SET is_done=false, queue_done=false, dispatched_at=NULL, completed_at=NULL -WHERE (is_done OR queue_done) AND EXISTS ( - SELECT 1 - FROM whatsapp_history_sync_message msg - WHERE msg.bridge_id=backfill_task.bridge_id - AND msg.user_login_id=backfill_task.user_login_id - AND msg.chat_jid=backfill_task.portal_id -); diff --git a/pkg/connector/wamsgtype.go b/pkg/connector/wamsgtype.go index 2b1458b7..0a29b829 100644 --- a/pkg/connector/wamsgtype.go +++ b/pkg/connector/wamsgtype.go @@ -142,10 +142,7 @@ func getMessageType(waMsg *waE2E.Message) string { case waMsg.StickerPackMessage != nil: return "sticker pack" case waMsg.AlbumMessage != nil: - if waMsg.GetAlbumMessage().GetContextInfo().GetStanzaID() == "" { - return "ignore" - } - return "album" + return "album" // or maybe these should be ignored? case waMsg.SendPaymentMessage != nil, waMsg.RequestPaymentMessage != nil, waMsg.DeclinePaymentRequestMessage != nil, waMsg.CancelPaymentRequestMessage != nil, waMsg.PaymentInviteMessage != nil: @@ -162,13 +159,3 @@ func getMessageType(waMsg *waE2E.Message) string { return "unknown" } } - -func needsPreviousEditPart(waMsg *waE2E.Message) bool { - switch { - case waMsg.ImageMessage != nil, waMsg.StickerMessage != nil, waMsg.VideoMessage != nil, - waMsg.PtvMessage != nil, waMsg.AudioMessage != nil, waMsg.DocumentMessage != nil: - return true - default: - return false - } -} diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index ba2adc23..57f9113c 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -27,15 +27,12 @@ import ( "image/color" "image/jpeg" "net/http" - "os" - "path/filepath" "slices" "strconv" "strings" "time" "github.com/rs/zerolog" - "go.mau.fi/util/exmime" "go.mau.fi/util/ffmpeg" "go.mau.fi/util/ptr" "go.mau.fi/util/random" @@ -56,7 +53,6 @@ import ( func (mc *MessageConverter) generateContextInfo( ctx context.Context, replyTo *database.Message, - relatesTo *event.RelatesTo, portal *bridgev2.Portal, perMessageTimer *event.BeeperDisappearingTimer, roomMention bool, @@ -67,7 +63,7 @@ func (mc *MessageConverter) generateContextInfo( if err == nil { contextInfo.StanzaID = proto.String(msgID.ID) contextInfo.Participant = proto.String(msgID.Sender.String()) - contextInfo.QuotedMessage = mc.getQuotedMessage(ctx, replyTo, relatesTo, portal) + contextInfo.QuotedMessage = &waE2E.Message{Conversation: proto.String("")} contextInfo.QuotedType = waE2E.ContextInfo_EXPLICIT.Enum() } else { zerolog.Ctx(ctx).Warn().Err(err). @@ -95,37 +91,6 @@ func (mc *MessageConverter) generateContextInfo( return contextInfo } -func (mc *MessageConverter) getQuotedMessage(ctx context.Context, replyTo *database.Message, relatesTo *event.RelatesTo, portal *bridgev2.Portal) *waE2E.Message { - log := zerolog.Ctx(ctx).With().Stringer("reply_to_event_id", replyTo.MXID).Logger() - if relatesTo.GetReplyTo() == replyTo.MXID && len(relatesTo.InReplyTo.BeeperQuote) > 0 { - var content event.MessageEventContent - if err := json.Unmarshal(relatesTo.InReplyTo.BeeperQuote, &content); err != nil { - log.Warn().Err(err).Msg("Failed to parse quoted event content") - } else { - return mc.convertQuote(ctx, content) - } - } - evt, err := mc.Bridge.Bot.GetEvent(ctx, portal.MXID, replyTo.MXID) - if err != nil { - log.Debug().Err(err).Msg("Failed to fetch quoted event") - } else if evt == nil { - log.Debug().Msg("Quoted event not found") - } else if evt.RoomID != "" && evt.RoomID != portal.MXID { - log.Warn().Msg("Quoted event is in a different room") - } else if evt.Unsigned.RedactedBecause == nil { - return mc.convertQuote(ctx, *evt.Content.AsMessage()) - } - return &waE2E.Message{Conversation: new("")} -} - -func (mc *MessageConverter) convertQuote(ctx context.Context, content event.MessageEventContent) *waE2E.Message { - content.Mentions = &event.Mentions{} - content.RemoveReplyFallback() - content.RemovePerMessageProfileFallback() - text, _ := mc.parseText(ctx, &content) - return &waE2E.Message{Conversation: &text} -} - func (mc *MessageConverter) ToWhatsApp( ctx context.Context, client *whatsmeow.Client, @@ -142,7 +107,7 @@ func (mc *MessageConverter) ToWhatsApp( } message := &waE2E.Message{} - contextInfo := mc.generateContextInfo(ctx, replyTo, content.RelatesTo, portal, content.BeeperDisappearingTimer, content.Mentions != nil && content.Mentions.Room) + contextInfo := mc.generateContextInfo(ctx, replyTo, portal, content.BeeperDisappearingTimer, content.Mentions != nil && content.Mentions.Room) switch content.MsgType { case event.MsgText, event.MsgNotice, event.MsgEmote: @@ -185,7 +150,7 @@ func (mc *MessageConverter) ToWhatsApp( } lid := parsedID.Sender if lid.Server == types.DefaultUserServer { - lid, err = getStore(client).LIDs.GetLIDForPN(ctx, lid) + lid, err = client.Store.LIDs.GetLIDForPN(ctx, lid) if err != nil { return nil, nil, fmt.Errorf("failed to get LID for PN: %w", err) } @@ -196,6 +161,7 @@ func (mc *MessageConverter) ToWhatsApp( DeprecatedLIDSession: ptr.Ptr(false), } } else { + // TODO check permissions? message.MessageContextInfo = &waE2E.MessageContextInfo{ MessageSecret: random.Bytes(32), } @@ -518,56 +484,6 @@ func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) { return webpBuffer.Bytes(), size, nil } -func convertUnsupportedVideoCodec(ctx context.Context, data []byte, mime string) ([]byte, string, error) { - if !ffmpeg.Supported() || !ffmpeg.ProbeSupported() { - return data, mime, nil - } - log := zerolog.Ctx(ctx) - tempdir, err := os.MkdirTemp("", "mautrix_whatsapp_video_*") - if err != nil { - log.Warn().Err(err).Msg("Failed to create temp dir for video, sending it as-is") - return data, mime, nil - } - defer os.RemoveAll(tempdir) - inputPath := filepath.Join(tempdir, "input"+exmime.ExtensionFromMimetype(mime)) - err = os.WriteFile(inputPath, data, 0600) - if err != nil { - log.Warn().Err(err).Msg("Failed to write video to temp file, sending it as-is") - return data, mime, nil - } - probe, err := ffmpeg.Probe(ctx, inputPath) - if err != nil { - log.Warn().Err(err).Msg("Failed to probe video, sending it as-is") - return data, mime, nil - } - videoSupported := !slices.ContainsFunc(probe.Streams, func(stream *ffmpeg.Stream) bool { - return stream.CodecType == "video" && stream.Disposition.AttachedPic == 0 && stream.CodecName != "h264" - }) - audioSupported := !slices.ContainsFunc(probe.Streams, func(stream *ffmpeg.Stream) bool { - return stream.CodecType == "audio" && stream.CodecName != "aac" - }) - if videoSupported && audioSupported { - return data, mime, nil - } - videoArgs := []string{"-c:v", "copy"} - if !videoSupported { - videoArgs = []string{ - "-pix_fmt", "yuv420p", "-c:v", "h264", "-b:v", "2M", - "-filter:v", "crop='floor(in_w/2)*2:floor(in_h/2)*2'", - } - } - outputPath := filepath.Join(tempdir, "output.mp4") - err = ffmpeg.ConvertPathWithDestination(ctx, inputPath, outputPath, nil, append(videoArgs, "-c:a", "aac", "-movflags", "+faststart"), false) - if err != nil { - return nil, mime, err - } - converted, err := os.ReadFile(outputPath) - if err != nil { - return nil, mime, err - } - return converted, "video/mp4", nil -} - func (mc *MessageConverter) getOriginalBridgedSticker(ctx context.Context, info *event.BridgedSticker) (*types.StickerPackItem, error) { if info == nil || info.Network != StickerSourceID || !strings.HasPrefix(info.PackURL, StickerPackURLPrefix) || info.ID == "" { return nil, nil @@ -662,17 +578,14 @@ func (mc *MessageConverter) reuploadFileToWhatsApp( case event.MsgVideo: switch mime { case "video/mp4", "video/3gpp": - data, mime, err = convertUnsupportedVideoCodec(ctx, data, mime) - if err != nil { - return nil, nil, mime, fmt.Errorf("%w (%s to h264): %w", bridgev2.ErrMediaConvertFailed, mime, err) - } + // allowed case "video/webm", "video/quicktime": sourceFormat := "webm" if mime == "video/quicktime" { sourceFormat = "mov" } data, err = ffmpeg.ConvertBytes(ctx, data, ".mp4", []string{"-f", sourceFormat}, []string{ - "-pix_fmt", "yuv420p", "-c:v", "h264", "-b:v", "2M", + "-pix_fmt", "yuv420p", "-c:v", "libx264", "-filter:v", "crop='floor(in_w/2)*2:floor(in_h/2)*2'", }, mime) if err != nil { @@ -681,7 +594,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp( mime = "video/mp4" case "image/gif": data, err = ffmpeg.ConvertBytes(ctx, data, ".mp4", []string{"-f", "gif"}, []string{ - "-pix_fmt", "yuv420p", "-c:v", "h264", "-b:v", "2M", "-movflags", "+faststart", + "-pix_fmt", "yuv420p", "-c:v", "libx264", "-movflags", "+faststart", "-filter:v", "crop='floor(in_w/2)*2:floor(in_h/2)*2'", }, mime) if err != nil { diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go index db18f35a..e912732d 100644 --- a/pkg/msgconv/from-whatsapp.go +++ b/pkg/msgconv/from-whatsapp.go @@ -17,7 +17,6 @@ package msgconv import ( - "cmp" "context" "fmt" "html" @@ -31,7 +30,6 @@ import ( "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waE2E" - "go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/types" _ "golang.org/x/image/webp" "maunium.net/go/mautrix/bridgev2" @@ -93,7 +91,7 @@ func (mc *MessageConverter) getBasicUserInfo(ctx context.Context, user types.JID portal := getPortal(ctx) login := mc.Bridge.GetCachedUserLoginByID(waid.MakeUserLoginID(pnJID)) if login != nil && (portal.Receiver == "" || portal.Receiver == login.ID) { - return login.UserMXID, cmp.Or(login.RemoteProfile.Name, ghost.Name), nil + return login.UserMXID, ghost.Name, nil } } return ghost.Intent.GetMXID(), ghost.Name, nil @@ -188,11 +186,11 @@ func (mc *MessageConverter) ToMatrix( case waMsg.EventMessage != nil: part, contextInfo = mc.convertEventMessage(ctx, waMsg.EventMessage) case waMsg.PinInChatMessage != nil: - part, contextInfo = mc.convertPinInChatMessage(ctx, info, waMsg.PinInChatMessage) + part, contextInfo = mc.convertPinInChatMessage(ctx, waMsg.PinInChatMessage) case waMsg.KeepInChatMessage != nil: part, contextInfo = mc.convertKeepInChatMessage(ctx, waMsg.KeepInChatMessage) case waMsg.RichResponseMessage != nil: - part, contextInfo = mc.convertRichResponseMessage(ctx, waMsg.RichResponseMessage, waMsg) + part, contextInfo = mc.convertRichResponseMessage(ctx, waMsg.RichResponseMessage) case waMsg.ImageMessage != nil: part, contextInfo = mc.convertMediaMessage(ctx, waMsg.ImageMessage, "photo", info, isViewOnce, previouslyConvertedPart) case waMsg.StickerMessage != nil: @@ -279,10 +277,10 @@ func (mc *MessageConverter) ToMatrix( var pn, lid types.JID if pcp.Server == types.DefaultUserServer { pn = pcp - lid, _ = getStore(client).LIDs.GetLIDForPN(ctx, pcp) + lid, _ = client.Store.LIDs.GetLIDForPN(ctx, pcp) } else if pcp.Server == types.HiddenUserServer { lid = pcp - pn, _ = getStore(client).LIDs.GetPNForLID(ctx, pcp) + pn, _ = client.Store.LIDs.GetPNForLID(ctx, pcp) } else if pcp.Server == types.BotServer { lid = pcp } @@ -321,16 +319,6 @@ func (mc *MessageConverter) ToMatrix( } cm.ThreadRoot = ptr.Ptr(waid.MakeMessageID(chat, pcp, commentTarget.GetID())) } - mc.cutOffLongMessage(part.Content) return cm } - -func getStore(cli *whatsmeow.Client) *store.Device { - if cli != nil { - if currentStore := cli.Store; currentStore != nil { - return currentStore - } - } - return store.NoopDevice -} diff --git a/pkg/msgconv/matrixpoll.go b/pkg/msgconv/matrixpoll.go index 0fe0026d..a5f02ea9 100644 --- a/pkg/msgconv/matrixpoll.go +++ b/pkg/msgconv/matrixpoll.go @@ -57,8 +57,8 @@ func (mc *MessageConverter) msc1767ToWhatsApp(ctx context.Context, msg event.MSC } var ( - errPollMissingQuestion = bridgev2.WrapErrorInStatus(errors.New("poll message is missing question")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) - errPollDuplicateOption = bridgev2.WrapErrorInStatus(errors.New("poll options must be unique")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) + errPollMissingQuestion = bridgev2.WrapErrorInStatus(errors.New("poll message is missing question")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported) + errPollDuplicateOption = bridgev2.WrapErrorInStatus(errors.New("poll options must be unique")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported) ) func (mc *MessageConverter) PollStartToWhatsApp( @@ -71,7 +71,7 @@ func (mc *MessageConverter) PollStartToWhatsApp( if maxAnswers >= len(content.PollStart.Answers) || maxAnswers < 0 { maxAnswers = 0 } - contextInfo := mc.generateContextInfo(ctx, replyTo, content.RelatesTo, portal, nil, content.Mentions != nil && content.Mentions.Room) + contextInfo := mc.generateContextInfo(ctx, replyTo, portal, nil, content.Mentions != nil && content.Mentions.Room) var question string question, contextInfo.MentionedJID = mc.msc1767ToWhatsApp(ctx, content.PollStart.Question, content.Mentions) if len(question) == 0 { @@ -150,8 +150,8 @@ func MessageIDToInfo(ctx context.Context, client *whatsmeow.Client, parsedMsgID chat := parsedMsgID.Chat sender := parsedMsgID.Sender if chat.Server == types.DefaultUserServer { - chatLID, _ := getStore(client).LIDs.GetLIDForPN(ctx, chat) - senderLID, _ := getStore(client).LIDs.GetLIDForPN(ctx, sender) + chatLID, _ := client.Store.LIDs.GetLIDForPN(ctx, chat) + senderLID, _ := client.Store.LIDs.GetLIDForPN(ctx, sender) if !chatLID.IsEmpty() && !senderLID.IsEmpty() { chat = chatLID sender = senderLID @@ -161,7 +161,7 @@ func MessageIDToInfo(ctx context.Context, client *whatsmeow.Client, parsedMsgID MessageSource: types.MessageSource{ Chat: chat, Sender: sender, - IsFromMe: sender.User == getStore(client).GetLID().User || sender.User == getStore(client).GetJID().User, + IsFromMe: sender.User == client.Store.GetLID().User || sender.User == client.Store.GetJID().User, IsGroup: chat.Server == types.GroupServer, }, ID: parsedMsgID.ID, diff --git a/pkg/msgconv/wa-contact.go b/pkg/msgconv/wa-contact.go index 38076939..7ca3f8e5 100644 --- a/pkg/msgconv/wa-contact.go +++ b/pkg/msgconv/wa-contact.go @@ -19,10 +19,8 @@ package msgconv import ( "context" "fmt" - "strings" "github.com/rs/zerolog" - "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow/proto/waE2E" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/event" @@ -67,17 +65,11 @@ func (mc *MessageConverter) convertContactMessage(ctx context.Context, msg *waE2 } func (mc *MessageConverter) convertContactsArrayMessage(ctx context.Context, msg *waE2E.ContactsArrayMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { - name := msg.GetDisplayName() - if len(name) == 0 { - name = fmt.Sprintf("%d contacts", len(msg.GetContacts())) - } - vcards := make([]string, len(msg.GetContacts())) - for i, contact := range msg.GetContacts() { - vcards[i] = strings.TrimSpace(contact.GetVcard()) - } - part, _ := mc.convertContactMessage(ctx, &waE2E.ContactMessage{ - DisplayName: &name, - Vcard: ptr.Ptr(strings.Join(vcards, "\n")), - }) - return part, msg.GetContextInfo() + return &bridgev2.ConvertedMessagePart{ + Type: event.EventMessage, + Content: &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: "Contact array messages are not yet supported", + }, + }, msg.GetContextInfo() } diff --git a/pkg/msgconv/wa-location.go b/pkg/msgconv/wa-location.go index 52f3f131..4a1e043f 100644 --- a/pkg/msgconv/wa-location.go +++ b/pkg/msgconv/wa-location.go @@ -30,40 +30,6 @@ import ( "maunium.net/go/mautrix/event" ) -func formatCoordinates(lat, long float64) string { - latChar := 'N' - if lat < 0 { - latChar = 'S' - } - longChar := 'E' - if long < 0 { - longChar = 'W' - } - return fmt.Sprintf("%.4f° %c %.4f° %c", math.Abs(lat), latChar, math.Abs(long), longChar) -} - -func addLocationThumbnail(ctx context.Context, content *event.MessageEventContent, thumbnail []byte) { - if len(thumbnail) == 0 { - return - } - thumbnailMime := http.DetectContentType(thumbnail) - thumbnailURL, thumbnailFile, err := getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, thumbnail, "thumb.jpeg", thumbnailMime) - if err != nil { - return - } - cfg, _, _ := image.DecodeConfig(bytes.NewReader(thumbnail)) - content.Info = &event.FileInfo{ - ThumbnailInfo: &event.FileInfo{ - Size: len(thumbnail), - Width: cfg.Width, - Height: cfg.Height, - MimeType: thumbnailMime, - }, - ThumbnailURL: thumbnailURL, - ThumbnailFile: thumbnailFile, - } -} - func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE2E.LocationMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { url := msg.GetURL() if len(url) == 0 { @@ -71,7 +37,15 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE } name := msg.GetName() if len(name) == 0 { - name = formatCoordinates(msg.GetDegreesLatitude(), msg.GetDegreesLongitude()) + latChar := 'N' + if msg.GetDegreesLatitude() < 0 { + latChar = 'S' + } + longChar := 'E' + if msg.GetDegreesLongitude() < 0 { + longChar = 'W' + } + name = fmt.Sprintf("%.4f° %c %.4f° %c", math.Abs(msg.GetDegreesLatitude()), latChar, math.Abs(msg.GetDegreesLongitude()), longChar) } content := &event.MessageEventContent{ @@ -81,7 +55,24 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE FormattedBody: fmt.Sprintf(`Location: %s
%s`, html.EscapeString(url), html.EscapeString(name), html.EscapeString(msg.GetAddress())), GeoURI: fmt.Sprintf("geo:%.5f,%.5f", msg.GetDegreesLatitude(), msg.GetDegreesLongitude()), } - addLocationThumbnail(ctx, content, msg.GetJPEGThumbnail()) + + if len(msg.GetJPEGThumbnail()) > 0 { + thumbnailMime := http.DetectContentType(msg.GetJPEGThumbnail()) + thumbnailURL, thumbnailFile, err := getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, msg.GetJPEGThumbnail(), "thumb.jpeg", thumbnailMime) + if err == nil { + cfg, _, _ := image.DecodeConfig(bytes.NewReader(msg.GetJPEGThumbnail())) + content.Info = &event.FileInfo{ + ThumbnailInfo: &event.FileInfo{ + Size: len(msg.GetJPEGThumbnail()), + Width: cfg.Width, + Height: cfg.Height, + MimeType: thumbnailMime, + }, + ThumbnailURL: thumbnailURL, + ThumbnailFile: thumbnailFile, + } + } + } return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, @@ -90,40 +81,14 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE } func (mc *MessageConverter) convertLiveLocationMessage(ctx context.Context, msg *waE2E.LiveLocationMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { - if msg.DegreesLatitude == nil || msg.DegreesLongitude == nil { - content := &event.MessageEventContent{ - Body: "Started sharing live location", - MsgType: event.MsgNotice, - } - if len(msg.GetCaption()) > 0 { - content.Body += ": " + msg.GetCaption() - } - content.Body += "\n\nUse the WhatsApp app to see the location." - return &bridgev2.ConvertedMessagePart{ - Type: event.EventMessage, - Content: content, - }, msg.GetContextInfo() - } - - lat, long := msg.GetDegreesLatitude(), msg.GetDegreesLongitude() - url := fmt.Sprintf("https://maps.google.com/?q=%.5f,%.5f", lat, long) - name := formatCoordinates(lat, long) - const note = "Live updates are only visible in the WhatsApp app." - body := fmt.Sprintf("Live location: %s\n%s", name, url) - formattedBody := fmt.Sprintf(`Live location: %s`, html.EscapeString(url), html.EscapeString(name)) - if caption := msg.GetCaption(); len(caption) > 0 { - body += "\n" + caption - formattedBody += "
" + html.EscapeString(caption) - } content := &event.MessageEventContent{ - MsgType: event.MsgLocation, - Body: body + "\n\n" + note, - Format: event.FormatHTML, - FormattedBody: formattedBody + "

" + note, - GeoURI: fmt.Sprintf("geo:%.5f,%.5f", lat, long), + Body: "Started sharing live location", + MsgType: event.MsgNotice, } - addLocationThumbnail(ctx, content, msg.GetJPEGThumbnail()) - + if len(msg.GetCaption()) > 0 { + content.Body += ": " + msg.GetCaption() + } + content.Body += "\n\nUse the WhatsApp app to see the location." return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, Content: content, diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go index 839f4c26..99648009 100644 --- a/pkg/msgconv/wa-media.go +++ b/pkg/msgconv/wa-media.go @@ -49,9 +49,6 @@ func (mc *MessageConverter) convertMediaMessage( isViewOnce bool, cachedPart *bridgev2.ConvertedMessagePart, ) (part *bridgev2.ConvertedMessagePart, contextInfo *waE2E.ContextInfo) { - if viewOnceMsg, ok := msg.(MediaMessageWithViewOnce); ok && viewOnceMsg.GetViewOnce() { - isViewOnce = true - } if mc.DisableViewOnce && isViewOnce { body := "You received a view once message. For added privacy, you can only open it on the WhatsApp app." if messageInfo.IsFromMe { @@ -94,9 +91,7 @@ func (mc *MessageConverter) convertMediaMessage( var err error portal := getPortal(ctx) idOverride := getEditTargetID(ctx) - preparedMedia.URL, preparedMedia.File, err = portal.Bridge.GenerateContentURI( - ctx, portal.MXID, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver, getMediaIDVersion(msg)), - ) + preparedMedia.URL, err = portal.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver, getMediaIDVersion(msg))) if err != nil { panic(fmt.Errorf("failed to generate content URI: %w", err)) } @@ -105,17 +100,13 @@ func (mc *MessageConverter) convertMediaMessage( if err != nil { panic(err) } - if mediaKeys.DirectPath == "" { - part = mc.makeMediaFailure(ctx, preparedMedia, mediaKeys, whatsmeow.ErrNoURLPresent) - } else { - part = &bridgev2.ConvertedMessagePart{ - Type: preparedMedia.Type, - Content: preparedMedia.MessageEventContent, - Extra: preparedMedia.Extra, - DBMetadata: &waid.MessageMetadata{ - DirectMediaMeta: directMediaMeta, - }, - } + part = &bridgev2.ConvertedMessagePart{ + Type: preparedMedia.Type, + Content: preparedMedia.MessageEventContent, + Extra: preparedMedia.Extra, + DBMetadata: &waid.MessageMetadata{ + DirectMediaMeta: directMediaMeta, + }, } } else if err := mc.reuploadWhatsAppAttachment(ctx, msg, preparedMedia); err != nil { part = mc.makeMediaFailure(ctx, preparedMedia, mediaKeys, err) @@ -235,11 +226,6 @@ type MediaMessageWithCaption interface { GetCaption() string } -type MediaMessageWithViewOnce interface { - MediaMessage - GetViewOnce() bool -} - type MediaMessageWithDimensions interface { MediaMessage GetHeight() uint32 @@ -263,10 +249,10 @@ func fixStickerDimensions(info *event.FileInfo) { info.Width = WhatsAppStickerSize info.Height = WhatsAppStickerSize } else if info.Width > info.Height { - info.Height = info.Height * WhatsAppStickerSize / info.Width + info.Height /= info.Width / WhatsAppStickerSize info.Width = WhatsAppStickerSize } else { - info.Width = info.Width * WhatsAppStickerSize / info.Height + info.Width /= info.Height / WhatsAppStickerSize info.Height = WhatsAppStickerSize } } diff --git a/pkg/msgconv/wa-misc.go b/pkg/msgconv/wa-misc.go index a0d70978..fa495e0c 100644 --- a/pkg/msgconv/wa-misc.go +++ b/pkg/msgconv/wa-misc.go @@ -199,9 +199,8 @@ func (mc *MessageConverter) convertMessageHistoryNotice(ctx context.Context, inf return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, Content: &event.MessageEventContent{ - MsgType: event.MsgNotice, - Body: body, - BeeperActionMessage: &event.BeeperActionMessage{Type: "message_history_notice"}, + MsgType: event.MsgNotice, + Body: body, }, }, msg.GetContextInfo() } @@ -322,19 +321,11 @@ func (mc *MessageConverter) convertEventMessage(ctx context.Context, msg *waE2E. }, msg.GetContextInfo() } -func (mc *MessageConverter) convertPinInChatMessage(ctx context.Context, info *types.MessageInfo, msg *waE2E.PinInChatMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { +func (mc *MessageConverter) convertPinInChatMessage(ctx context.Context, msg *waE2E.PinInChatMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { body := "Pinned a message" if msg.GetType() == waE2E.PinInChatMessage_UNPIN_FOR_ALL { body = "Unpinned a message" } - var contextInfo *waE2E.ContextInfo - if target, err := waid.ParseMessageID(KeyToMessageID(ctx, getClient(ctx), info.Chat, info.Sender, msg.GetKey())); err == nil { - contextInfo = &waE2E.ContextInfo{ - StanzaID: proto.String(target.ID), - Participant: proto.String(target.Sender.String()), - RemoteJID: proto.String(target.Chat.String()), - } - } return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, @@ -342,7 +333,7 @@ func (mc *MessageConverter) convertPinInChatMessage(ctx context.Context, info *t MsgType: event.MsgNotice, Body: body, }, - }, contextInfo + }, nil } func (mc *MessageConverter) convertKeepInChatMessage(ctx context.Context, msg *waE2E.KeepInChatMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { @@ -360,11 +351,10 @@ func (mc *MessageConverter) convertKeepInChatMessage(ctx context.Context, msg *w }, nil } -func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage, fullMsg *waE2E.Message) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { - if msg.GetUnifiedResponse() != nil { - return mc.convertUnifiedRichResponseMessage(ctx, msg.GetUnifiedResponse().GetData()), msg.GetContextInfo() - } +func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { var body strings.Builder + + // TODO switch to new format? for i, submsg := range msg.GetSubmessages() { if submsg.GetMessageType() == waAICommonDeprecated.AIRichResponseSubMessageType_AI_RICH_RESPONSE_TEXT { if i > 0 { @@ -373,14 +363,10 @@ func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg body.WriteString(submsg.GetMessageText()) } } - unknownPart, _ := mc.convertUnknownMessage(ctx, fullMsg) - if body.Len() == 0 { - return unknownPart, msg.GetContextInfo() - } + content := format.RenderMarkdown(body.String(), true, false) return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, Content: &content, - Extra: unknownPart.Extra, }, msg.GetContextInfo() } diff --git a/pkg/msgconv/wa-poll.go b/pkg/msgconv/wa-poll.go index 2bcf671c..881a5fb0 100644 --- a/pkg/msgconv/wa-poll.go +++ b/pkg/msgconv/wa-poll.go @@ -108,7 +108,7 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender sender.Server = types.DefaultUserServer } } else if chat.Server == types.DefaultUserServer || chat.Server == types.HiddenUserServer || chat.Server == types.BotServer { - if sender.User == getStore(client).GetJID().User || sender.User == getStore(client).GetLID().User { + if sender.User == client.Store.GetJID().User || sender.User == client.Store.GetLID().User { // Message key is not from the sender, but message sender (containing key) is me, // so message key sender is the other user in the DM sender = chat @@ -116,9 +116,9 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender // Message key is not from the sender, but message sender (containing key) is not me, // so message key sender is me if chat.Server == types.HiddenUserServer { - sender = getStore(client).GetLID().ToNonAD() + sender = client.Store.GetLID().ToNonAD() } else { - sender = getStore(client).GetJID().ToNonAD() + sender = client.Store.GetJID().ToNonAD() } } } else { diff --git a/pkg/msgconv/wa-rich-response.go b/pkg/msgconv/wa-rich-response.go deleted file mode 100644 index e506a64e..00000000 --- a/pkg/msgconv/wa-rich-response.go +++ /dev/null @@ -1,189 +0,0 @@ -// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2026 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package msgconv - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "html" - "strings" - - "github.com/rs/zerolog" - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/extension" - "go.mau.fi/whatsmeow/types/richresponse" - "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/crypto/canonicaljson" - "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/format" - "maunium.net/go/mautrix/format/mdext" -) - -func (mc *MessageConverter) convertUnifiedRichResponseMessage(ctx context.Context, data []byte) *bridgev2.ConvertedMessagePart { - extra := map[string]any{} - _, canonicalErr := canonicaljson.Marshal(json.RawMessage(data)) - if canonicalErr == nil && len(data) < 24*1024 { - extra["fi.mau.whatsapp.rich_message"] = json.RawMessage(data) - } else if len(data) < 16*1024 { - extra["fi.mau.whatsapp.non_json_rich_message"] = base64.StdEncoding.EncodeToString(data) - } - var rrMsg richresponse.RichResponse - err := json.Unmarshal(data, &rrMsg) - if err != nil { - zerolog.Ctx(ctx).Err(err).Msg("Failed to unmarshal unified rich response message") - rrMsg.Sections = nil - } - var htmlBuf strings.Builder - for _, mv := range rrMsg.Sections { - if mv.Model == nil { - continue - } - for _, p := range mv.Model.GetPrimitives() { - mc.convertRichResponsePrimitive(ctx, p, &htmlBuf) - } - } - if htmlBuf.Len() == 0 { - return &bridgev2.ConvertedMessagePart{ - Type: event.EventMessage, - Content: &event.MessageEventContent{ - MsgType: event.MsgNotice, - Body: "Unknown message type, please view it on the WhatsApp app", - }, - Extra: extra, - } - } - content := format.HTMLToContent(htmlBuf.String()) - if len(data)+len(content.Body)+len(content.FormattedBody) > 40000 { - delete(extra, "fi.mau.whatsapp.rich_message") - delete(extra, "fi.mau.whatsapp.non_json_rich_message") - } - extra["fi.mau.whatsapp.rich_response_id"] = rrMsg.ResponseID - return &bridgev2.ConvertedMessagePart{ - Type: event.EventMessage, - Content: &content, - Extra: extra, - } -} - -var mdRender = goldmark.New(format.HTMLOptions, goldmark.WithExtensions(extension.Strikethrough, extension.Table, mdext.EscapeHTML, mdext.ShortEmphasis, mdext.Math)) - -func (mc *MessageConverter) convertRichMarkdownText(ctx context.Context, text *richresponse.GenAIMarkdownTextUXPrimitive, buf *strings.Builder) error { - for _, ent := range text.InlineEntities { - switch meta := ent.Metadata.(type) { - //case *richresponse.GenAISearchCitationItem: - case *richresponse.GenAIInlineLinkItem: - text.Text = strings.ReplaceAll( - text.Text, - fmt.Sprintf("{{%s}}", ent.Key), - fmt.Sprintf("[%s](%s)", meta.DisplayName, meta.URL), - ) - case *richresponse.GenAIDeepLinkItem: - text.Text = strings.ReplaceAll( - text.Text, - fmt.Sprintf("{{%s}}", ent.Key), - meta.Text, - ) - case *richresponse.GenAILatexItem: - text.Text = strings.ReplaceAll( - text.Text, - fmt.Sprintf("{{%s}}", ent.Key), - fmt.Sprintf(`%[1]s`, html.EscapeString(meta.LatexExpression)), - ) - default: - text.Text = strings.ReplaceAll( - text.Text, - fmt.Sprintf("{{%s}}", ent.Key), - ent.Key, - ) - } - } - return mdRender.Convert([]byte(text.Text), buf) -} - -func (mc *MessageConverter) convertRichResponsePrimitive(ctx context.Context, primitive richresponse.Primitive, buf *strings.Builder) error { - switch p := primitive.(type) { - case *richresponse.GenAICodeUXPrimitive: - if p.Language != "" { - _, _ = fmt.Fprintf(buf, `
`, html.EscapeString(p.Language))
-		} else {
-			buf.WriteString("
")
-		}
-		for _, part := range p.CodeBlocks {
-			buf.WriteString(html.EscapeString(part.Content))
-		}
-		buf.WriteString("
") - case *richresponse.GenAIMarkdownTextUXPrimitive: - return mc.convertRichMarkdownText(ctx, p, buf) - case *richresponse.GenATableUXPrimitive: - buf.WriteString("") - for _, row := range p.Rows { - buf.WriteString("") - cellType := "td" - if row.IsHeader { - cellType = "th" - } - if len(row.MarkdownCells) > 0 { - for _, cell := range row.MarkdownCells { - _, _ = fmt.Fprintf(buf, "<%s>", cellType) - err := mc.convertRichMarkdownText(ctx, &cell, buf) - if err != nil { - return err - } - _, _ = fmt.Fprintf(buf, "", cellType) - } - } else { - for _, cell := range row.Cells { - _, _ = fmt.Fprintf(buf, "<%s>%s", cellType, html.EscapeString(cell), cellType) - } - } - buf.WriteString("") - } - buf.WriteString("
") - case *richresponse.GenAILatexUXPrimitive: - _, _ = fmt.Fprintf(buf, `%[1]s`, html.EscapeString(p.GetLatexExpression())) - case *richresponse.FOATextPrimitive: - // TODO remove placeholders? - return mdRender.Convert([]byte(p.Text), buf) - case *richresponse.GenAIMetadataTextPrimitive: - return mdRender.Convert([]byte(p.Text), buf) - //case *richresponse.GenAIBotThinkingStatusPrimitive: - //case *richresponse.GenAIProductItemCardPrimitive: - //case *richresponse.GenAIImagePrimitive: - //case *richresponse.GenAITaskPrimitive: - //case *richresponse.GenAIReelPrimitive: - //case *richresponse.GenAIPostPrimitive: - //case *richresponse.GenAIImaginePrimitive: - //case *richresponse.GenAISearchResultPrimitive: - //case *richresponse.FOABloksPrimitive: - case *richresponse.GenAIDividerPrimitive: - // TODO dots? - buf.WriteString("
") - case *richresponse.GenAISpacerPrimitive: - if p.Spacing <= 1 { - buf.WriteString("
") - } else { - for range p.Spacing { - buf.WriteString("
") - } - } - default: - buf.WriteString("

Unknown primitive type

") - } - return nil -} diff --git a/pkg/msgconv/wa-text.go b/pkg/msgconv/wa-text.go index 52874fd8..032e3fd8 100644 --- a/pkg/msgconv/wa-text.go +++ b/pkg/msgconv/wa-text.go @@ -21,7 +21,6 @@ import ( "fmt" "html" "regexp" - "slices" "strconv" "strings" @@ -47,31 +46,6 @@ func (mc *MessageConverter) convertTextMessage(ctx context.Context, msg *waE2E.M return } -func (mc *MessageConverter) cutOffLongMessage(content *event.MessageEventContent) { - size := len(content.Body) + len(content.FormattedBody) - if content.Mentions != nil { - for _, userID := range content.Mentions.UserIDs { - size += len(userID) - } - } - if size <= 40000 { - return - } - if content.Mentions != nil { - content.Mentions.UserIDs = slices.DeleteFunc(content.Mentions.UserIDs, mc.Bridge.IsGhostMXID) - } - if content.FormattedBody == "" { - if len(content.Body) > 40000 { - content.Body = content.Body[:40000] + "\n\n[long message cut off]" - } - } else if len(content.FormattedBody) > 20000 && len(content.Body) > 500 { - content.Body = content.Body[:500] + "[...]" - } - if len(content.FormattedBody) > 40000 { - content.FormattedBody = content.FormattedBody[:40000] + "
[long message cut off]" - } -} - func (mc *MessageConverter) parseFormatting(content *event.MessageEventContent, allowInlineURL, forceHTML bool) { parsedHTML := parseWAFormattingToHTML(content.Body, allowInlineURL) if forceHTML || parsedHTML != event.TextToHTML(content.Body) { diff --git a/pkg/waid/dbmeta.go b/pkg/waid/dbmeta.go index b1a47e0e..1e544267 100644 --- a/pkg/waid/dbmeta.go +++ b/pkg/waid/dbmeta.go @@ -37,17 +37,12 @@ type UserLoginMetadata struct { PushKeys *PushKeys `json:"push_keys,omitempty"` APNSEncPubKey []byte `json:"apns_enc_pubkey,omitempty"` APNSEncPrivKey []byte `json:"apns_enc_privkey,omitempty"` - LoggedInAt jsontime.Unix `json:"logged_in_at,omitzero"` + LoggedInAt jsontime.Unix `json:"logged_in_at,omitempty"` AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"` - WASAResynced bool `json:"wasa_resynced,omitzero"` - HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitzero"` - ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitzero"` - - PreviousOwnLIDs []types.JID `json:"previous_own_lids,omitempty"` - - Blocklist *types.Blocklist `json:"blocklist,omitempty"` + HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"` + ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitempty"` MData json.RawMessage `json:"mdata,omitempty"` } @@ -93,13 +88,13 @@ type GroupInviteMeta struct { } type MessageMetadata struct { - SenderDeviceID uint16 `json:"sender_device_id,omitzero"` + SenderDeviceID uint16 `json:"sender_device_id,omitempty"` Error MessageErrorType `json:"error,omitempty"` BroadcastListJID *types.JID `json:"broadcast_list_jid,omitempty"` GroupInvite *GroupInviteMeta `json:"group_invite,omitempty"` FailedMediaMeta json.RawMessage `json:"media_meta,omitempty"` DirectMediaMeta json.RawMessage `json:"direct_media_meta,omitempty"` - IsMatrixPoll bool `json:"is_matrix_poll,omitzero"` + IsMatrixPoll bool `json:"is_matrix_poll,omitempty"` Edits []types.MessageID `json:"edits,omitempty"` } @@ -127,16 +122,14 @@ type ReactionMetadata struct { } type PortalMetadata struct { - DisappearingTimerSetAt int64 `json:"disappearing_timer_set_at,omitzero"` + DisappearingTimerSetAt int64 `json:"disappearing_timer_set_at,omitempty"` TopicID string `json:"topic_id,omitempty"` - LastSync jsontime.Unix `json:"last_sync,omitzero"` - CommunityAnnouncementGroup bool `json:"is_cag,omitzero"` + LastSync jsontime.Unix `json:"last_sync,omitempty"` + CommunityAnnouncementGroup bool `json:"is_cag,omitempty"` AddressingMode types.AddressingMode `json:"addressing_mode,omitempty"` - LIDMigrationAttempted bool `json:"lid_migration_attempted,omitzero"` + LIDMigrationAttempted bool `json:"lid_migration_attempted,omitempty"` } type GhostMetadata struct { - LastSync jsontime.Unix `json:"last_sync,omitzero"` - - DirectAvatarURL string `json:"direct_avatar_url,omitempty"` + LastSync jsontime.Unix `json:"last_sync,omitempty"` } diff --git a/pkg/waid/id.go b/pkg/waid/id.go index 6edbcb81..7f8fe5bf 100644 --- a/pkg/waid/id.go +++ b/pkg/waid/id.go @@ -84,14 +84,10 @@ func MakeMessageID(chat, sender types.JID, id types.MessageID) networkid.Message } func MakeMessageIDWithAltSender(chat, sender, altSender types.JID, id types.MessageID) networkid.MessageID { - return MakeMessageID(chat, pickMessageIDSender(chat, sender, altSender), id) -} - -func pickMessageIDSender(chat, sender, altSender types.JID) types.JID { if chat.Server == types.HiddenUserServer && sender.Server == types.DefaultUserServer && altSender.Server == types.HiddenUserServer { - return altSender + sender = altSender } - return sender + return MakeMessageID(chat, sender, id) } func MakeFakeMessageID(chat, sender types.JID, data string) networkid.MessageID { diff --git a/pkg/waid/mediaid.go b/pkg/waid/mediaid.go index 913a23bb..716e187b 100644 --- a/pkg/waid/mediaid.go +++ b/pkg/waid/mediaid.go @@ -24,7 +24,6 @@ import ( "strconv" "strings" - "go.mau.fi/util/random" "go.mau.fi/whatsmeow/types" "maunium.net/go/mautrix/bridgev2/networkid" ) @@ -39,7 +38,7 @@ const ( func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID, version []byte) networkid.MediaID { compactChat := compactJID(messageInfo.Chat.ToNonAD()) - compactSender := compactJID(pickMessageIDSender(messageInfo.Chat, messageInfo.Sender, messageInfo.SenderAlt).ToNonAD()) + compactSender := compactJID(messageInfo.Sender.ToNonAD()) receiverID := compactJID(ParseUserLoginID(receiver, 0)) var compactID []byte if idOverride != "" { @@ -86,16 +85,6 @@ type AvatarMediaInfo struct { 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 { receiverID := compactJID(ParseUserLoginID(receiver, 0)) mediaID := make([]byte, 0, 4+len(packID)+len(fileHash)+len(receiverID))