mirror of
https://github.com/mautrix/whatsapp.git
synced 2026-05-14 17:56:53 -04:00
Compare commits
2 commits
main
...
kb/history
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a7217e0ff | ||
|
|
7ee5aa647c |
22 changed files with 358 additions and 997 deletions
14
.github/ISSUE_TEMPLATE/bug.md
vendored
14
.github/ISSUE_TEMPLATE/bug.md
vendored
|
|
@ -7,12 +7,10 @@ type: Bug
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Include relevant logs, the bridge version and other important details here -->
|
<!--
|
||||||
|
Remember to include relevant logs, the bridge version and any other details.
|
||||||
|
|
||||||
### Checklist
|
It's always best to ask in the Matrix room first, especially if you aren't sure
|
||||||
|
what details are needed. Issues with insufficient detail will likely just be
|
||||||
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. -->
|
ignored or closed immediately.
|
||||||
|
-->
|
||||||
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
|
|
||||||
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
|
|
||||||
* [ ] The bug is still present on the main branch. The `!wa version` command output is: ``
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,3 @@
|
||||||
# v26.04
|
|
||||||
|
|
||||||
* Added support for @room mentions in both directions.
|
|
||||||
* Changed initial backfill to happen even if WhatsApp doesn't send full history.
|
|
||||||
* Fixed panic when handling updates to unknown polls from WhatsApp.
|
|
||||||
* Fixed some background loops not stopping when a user is logged out.
|
|
||||||
|
|
||||||
# v26.03
|
# v26.03
|
||||||
|
|
||||||
* Added option to save outgoing messages in the database to allow encryption
|
* Added option to save outgoing messages in the database to allow encryption
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ var m = mxmain.BridgeMain{
|
||||||
Name: "mautrix-whatsapp",
|
Name: "mautrix-whatsapp",
|
||||||
URL: "https://github.com/mautrix/whatsapp",
|
URL: "https://github.com/mautrix/whatsapp",
|
||||||
Description: "A Matrix-WhatsApp puppeting bridge.",
|
Description: "A Matrix-WhatsApp puppeting bridge.",
|
||||||
Version: "26.04",
|
Version: "26.03",
|
||||||
SemCalVer: true,
|
SemCalVer: true,
|
||||||
Connector: &connector.WhatsAppConnector{},
|
Connector: &connector.WhatsAppConnector{},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
go.mod
36
go.mod
|
|
@ -2,52 +2,52 @@ module go.mau.fi/mautrix-whatsapp
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
toolchain go1.26.2
|
toolchain go1.26.1
|
||||||
|
|
||||||
tool go.mau.fi/util/cmd/maubuild
|
tool go.mau.fi/util/cmd/maubuild
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/lib/pq v1.12.3
|
github.com/lib/pq v1.11.2
|
||||||
github.com/rs/zerolog v1.35.1
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/tidwall/gjson v1.18.0
|
go.mau.fi/util v0.9.7
|
||||||
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25
|
|
||||||
go.mau.fi/webp v0.2.0
|
go.mau.fi/webp v0.2.0
|
||||||
go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f
|
go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4
|
||||||
golang.org/x/image v0.39.0
|
golang.org/x/image v0.37.0
|
||||||
golang.org/x/net v0.53.0
|
golang.org/x/net v0.52.0
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.20.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4
|
maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.2.0 // indirect
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
github.com/beeper/argo-go v1.1.2 // indirect
|
github.com/beeper/argo-go v1.1.2 // indirect
|
||||||
github.com/coder/websocket v1.8.14 // indirect
|
github.com/coder/websocket v1.8.14 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
|
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
|
||||||
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.44 // indirect
|
github.com/mattn/go-sqlite3 v1.14.34 // indirect
|
||||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||||
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
github.com/tidwall/match v1.2.0 // indirect
|
github.com/tidwall/match v1.2.0 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
|
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
|
||||||
github.com/yuin/goldmark v1.8.2 // indirect
|
github.com/yuin/goldmark v1.7.16 // indirect
|
||||||
go.mau.fi/libsignal v0.2.1 // indirect
|
go.mau.fi/libsignal v0.2.1 // indirect
|
||||||
go.mau.fi/zeroconfig v0.2.0 // indirect
|
go.mau.fi/zeroconfig v0.2.0 // indirect
|
||||||
golang.org/x/crypto v0.50.0 // indirect
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||||
golang.org/x/mod v0.35.0 // indirect
|
golang.org/x/mod v0.34.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.36.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
maunium.net/go/mauflag v1.0.0 // indirect
|
maunium.net/go/mauflag v1.0.0 // indirect
|
||||||
|
|
|
||||||
72
go.sum
72
go.sum
|
|
@ -10,13 +10,15 @@ github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
|
||||||
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
|
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||||
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
||||||
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
|
@ -28,17 +30,21 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
|
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||||
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
|
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 h1:rh2lKw/P/EqHa724vYH2+VVQ1YnW4u6EOXl0PMAovZE=
|
||||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
|
@ -46,8 +52,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
|
|
@ -67,35 +73,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/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
|
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
|
||||||
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
|
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
|
||||||
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25 h1:YPEmc+li7TF6C9AdRTcSLMb6yCHdF27/wNT7kFLIVNg=
|
go.mau.fi/util v0.9.7 h1:AWGNbJfz1zRcQOKeOEYhKUG2fT+/26Gy6kyqcH8tnBg=
|
||||||
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
|
go.mau.fi/util v0.9.7/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE=
|
||||||
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
|
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
|
||||||
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
|
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
|
||||||
go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f h1:icWtsD1MH5nlo8mEpHMPZ9+1kgHkjmXQroYi0lHXKZ0=
|
go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4 h1:FGA3NtCVNeCJ+C+KBg1pODsrfxC/trM3RHFWIeY7y4c=
|
||||||
go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
|
go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM=
|
||||||
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
|
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
|
||||||
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
|
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
|
||||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
@ -107,5 +115,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||||
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4 h1:zNC9eVAhw8FhKpM3AxNAh/iy75UEYX91uJUvqqAYlvo=
|
maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7 h1:KUhlBHWGgknqYC2V8di4DFNh73atDtgPlqqO5FoLmPc=
|
||||||
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4/go.mod h1:3sOGhXi3P1V6/NruTA0gujkvTypXVUraWktCuTGyDuM=
|
maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7/go.mod h1:YWw8NWTszsbyFAznboicBObwHPgTSLcuTbVX2kY7U2M=
|
||||||
|
|
|
||||||
|
|
@ -115,30 +115,14 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
|
||||||
Uint32("chunk_order", evt.GetChunkOrder()).
|
Uint32("chunk_order", evt.GetChunkOrder()).
|
||||||
Uint32("progress", evt.GetProgress()).
|
Uint32("progress", evt.GetProgress()).
|
||||||
Logger()
|
Logger()
|
||||||
log.Debug().
|
log.Debug().Msg("Downloading history sync")
|
||||||
Int64("oldest_msg_in_chunk_ts", evt.GetOldestMsgInChunkTimestampSec()).
|
|
||||||
Any("full_request_meta", evt.GetFullHistorySyncOnDemandRequestMetadata()).
|
|
||||||
Any("access_status", evt.GetMessageAccessStatus()).
|
|
||||||
Str("peer_data_request_session_id", evt.GetPeerDataRequestSessionID()).
|
|
||||||
Msg("Downloading history sync")
|
|
||||||
blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true)
|
blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to download history sync")
|
log.Err(err).Msg("Failed to download history sync")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND {
|
|
||||||
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")
|
|
||||||
} else if err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle()); err != nil {
|
|
||||||
log.Err(err).Msg("Failed to delete history sync blob from server")
|
|
||||||
} else {
|
|
||||||
log.Debug().Msg("Finished handling on-demand history sync and deleted history sync blob from server")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = wa.Main.DB.DoTxn(ctx, nil, func(ctx context.Context) (innerErr error) {
|
err = wa.Main.DB.DoTxn(ctx, nil, func(ctx context.Context) (innerErr error) {
|
||||||
innerErr = wa.handleWAHistorySync(ctx, evt, blob, true)
|
resetTimer, innerErr = wa.handleWAHistorySync(ctx, evt, blob, true)
|
||||||
if innerErr != nil {
|
if innerErr != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -150,16 +134,6 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to store history sync notification data")
|
log.Err(err).Msg("Failed to store history sync notification data")
|
||||||
} else {
|
|
||||||
resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP ||
|
|
||||||
blob.GetSyncType() == waHistorySync.HistorySync_RECENT ||
|
|
||||||
blob.GetSyncType() == waHistorySync.HistorySync_FULL
|
|
||||||
err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle())
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Failed to delete history sync blob from server")
|
|
||||||
} else {
|
|
||||||
log.Debug().Msg("Deleted history sync blob from server")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -169,9 +143,9 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
notif *waE2E.HistorySyncNotification,
|
notif *waE2E.HistorySyncNotification,
|
||||||
evt *waHistorySync.HistorySync,
|
evt *waHistorySync.HistorySync,
|
||||||
stopOnError bool,
|
stopOnError bool,
|
||||||
) error {
|
) (bool, error) {
|
||||||
if evt == nil || evt.SyncType == nil {
|
if evt == nil || evt.SyncType == nil {
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
log := wa.UserLogin.Log.With().
|
log := wa.UserLogin.Log.With().
|
||||||
Str("action", "store history sync").
|
Str("action", "store history sync").
|
||||||
|
|
@ -196,7 +170,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
Int("recent_sticker_count", len(evt.GetRecentStickers())).
|
Int("recent_sticker_count", len(evt.GetRecentStickers())).
|
||||||
Int("past_participant_count", len(evt.GetPastParticipants())).
|
Int("past_participant_count", len(evt.GetPastParticipants())).
|
||||||
Msg("Ignoring history sync")
|
Msg("Ignoring history sync")
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
log.Info().
|
log.Info().
|
||||||
Int("conversation_count", len(evt.GetConversations())).
|
Int("conversation_count", len(evt.GetConversations())).
|
||||||
|
|
@ -204,8 +178,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
Dict("notification_metadata", zerolog.Dict().
|
Dict("notification_metadata", zerolog.Dict().
|
||||||
Int64("oldest_msg_in_chunk_ts", notif.GetOldestMsgInChunkTimestampSec()).
|
Int64("oldest_msg_in_chunk_ts", notif.GetOldestMsgInChunkTimestampSec()).
|
||||||
Any("full_request_meta", notif.GetFullHistorySyncOnDemandRequestMetadata()).
|
Any("full_request_meta", notif.GetFullHistorySyncOnDemandRequestMetadata()).
|
||||||
Any("access_status", notif.GetMessageAccessStatus()).
|
Any("access_status", notif.GetMessageAccessStatus())).
|
||||||
Str("peer_data_request_session_id", notif.GetPeerDataRequestSessionID())).
|
|
||||||
Msg("Storing history sync")
|
Msg("Storing history sync")
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
successfullySavedTotal := 0
|
successfullySavedTotal := 0
|
||||||
|
|
@ -246,7 +219,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
return c.Stringer("chat_jid", jid)
|
return c.Stringer("chat_jid", jid)
|
||||||
})
|
})
|
||||||
|
|
||||||
var minTime, maxTime, firstItemTime, lastItemTime time.Time
|
var minTime, maxTime time.Time
|
||||||
var minTimeIndex, maxTimeIndex int
|
var minTimeIndex, maxTimeIndex int
|
||||||
|
|
||||||
ignoredTypes := 0
|
ignoredTypes := 0
|
||||||
|
|
@ -262,10 +235,6 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
Msg("Dropping historical message due to parse error")
|
Msg("Dropping historical message due to parse error")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if firstItemTime.IsZero() {
|
|
||||||
firstItemTime = msgEvt.Info.Timestamp
|
|
||||||
}
|
|
||||||
lastItemTime = msgEvt.Info.Timestamp
|
|
||||||
if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) {
|
if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) {
|
||||||
minTime = msgEvt.Info.Timestamp
|
minTime = msgEvt.Info.Timestamp
|
||||||
minTimeIndex = i
|
minTimeIndex = i
|
||||||
|
|
@ -298,9 +267,6 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
Int("lowest_time_index", minTimeIndex).
|
Int("lowest_time_index", minTimeIndex).
|
||||||
Time("highest_time", maxTime).
|
Time("highest_time", maxTime).
|
||||||
Int("highest_time_index", maxTimeIndex).
|
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().
|
Dict("metadata", zerolog.Dict().
|
||||||
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
|
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
|
||||||
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
|
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
|
||||||
|
|
@ -309,9 +275,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
Bool("archived", conv.GetArchived()).
|
Bool("archived", conv.GetArchived()).
|
||||||
Uint32("pinned", conv.GetPinned()).
|
Uint32("pinned", conv.GetPinned()).
|
||||||
Uint64("mute_end", conv.GetMuteEndTime()).
|
Uint64("mute_end", conv.GetMuteEndTime()).
|
||||||
Uint32("unread_count", conv.GetUnreadCount()).
|
Uint32("unread_count", conv.GetUnreadCount()),
|
||||||
Bool("end_of_history", conv.GetEndOfHistoryTransfer()).
|
|
||||||
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()),
|
|
||||||
).
|
).
|
||||||
Msg("Collected messages to save from history sync conversation")
|
Msg("Collected messages to save from history sync conversation")
|
||||||
|
|
||||||
|
|
@ -319,7 +283,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime))
|
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if stopOnError {
|
if stopOnError {
|
||||||
return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
|
return false, fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
|
||||||
}
|
}
|
||||||
log.Err(err).Msg("Failed to save conversation metadata")
|
log.Err(err).Msg("Failed to save conversation metadata")
|
||||||
continue
|
continue
|
||||||
|
|
@ -327,7 +291,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages)
|
err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if stopOnError {
|
if stopOnError {
|
||||||
return fmt.Errorf("failed to save messages in %s: %w", jid, err)
|
return false, fmt.Errorf("failed to save messages in %s: %w", jid, err)
|
||||||
}
|
}
|
||||||
log.Err(err).Msg("Failed to save messages")
|
log.Err(err).Msg("Failed to save messages")
|
||||||
failedToSaveTotal += len(messages)
|
failedToSaveTotal += len(messages)
|
||||||
|
|
@ -337,7 +301,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
err = wa.Main.Bridge.DB.BackfillTask.MarkNotDone(ctx, wa.makeWAPortalKey(jid), wa.UserLogin.ID)
|
err = wa.Main.Bridge.DB.BackfillTask.MarkNotDone(ctx, wa.makeWAPortalKey(jid), wa.UserLogin.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if stopOnError {
|
if stopOnError {
|
||||||
return fmt.Errorf("failed to mark backfill task as not done for %s: %w", jid, err)
|
return false, fmt.Errorf("failed to mark backfill task as not done for %s: %w", jid, err)
|
||||||
}
|
}
|
||||||
log.Err(err).Msg("Failed to mark backfill task as not done")
|
log.Err(err).Msg("Failed to mark backfill task as not done")
|
||||||
}
|
}
|
||||||
|
|
@ -349,7 +313,9 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
Int("total_message_count", totalMessageCount).
|
Int("total_message_count", totalMessageCount).
|
||||||
Dur("duration", time.Since(start)).
|
Dur("duration", time.Since(start)).
|
||||||
Msg("Finished storing history sync")
|
Msg("Finished storing history sync")
|
||||||
return nil
|
resetTimer := evt.GetSyncType() == waHistorySync.HistorySync_RECENT ||
|
||||||
|
evt.GetSyncType() == waHistorySync.HistorySync_FULL
|
||||||
|
return resetTimer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
||||||
|
|
@ -473,34 +439,24 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
||||||
}
|
}
|
||||||
var markRead bool
|
var markRead bool
|
||||||
var startTime, endTime *time.Time
|
var startTime, endTime *time.Time
|
||||||
var conv *wadb.Conversation
|
|
||||||
if params.Forward || wa.Main.Config.HistorySync.BackwardsOnDemand {
|
|
||||||
conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if params.Forward {
|
if params.Forward {
|
||||||
if params.AnchorMessage != nil {
|
if params.AnchorMessage != nil {
|
||||||
startTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
startTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||||
}
|
}
|
||||||
if conv != nil {
|
conv, err := wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
|
||||||
|
} else if conv != nil {
|
||||||
markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0
|
markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0
|
||||||
}
|
}
|
||||||
} else {
|
} else if params.Cursor != "" {
|
||||||
if params.AnchorMessage != nil {
|
endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
|
||||||
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
if err != nil {
|
||||||
}
|
return nil, fmt.Errorf("failed to parse cursor: %w", err)
|
||||||
if params.Cursor != "" {
|
|
||||||
endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse cursor: %w", err)
|
|
||||||
}
|
|
||||||
cursorTime := time.Unix(endTimeUnix, 0)
|
|
||||||
if endTime == nil || cursorTime.Before(*endTime) {
|
|
||||||
endTime = &cursorTime
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
endTime = ptr.Ptr(time.Unix(endTimeUnix, 0))
|
||||||
|
} else if params.AnchorMessage != nil {
|
||||||
|
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||||
}
|
}
|
||||||
var anchorID types.MessageID
|
var anchorID types.MessageID
|
||||||
if params.AnchorMessage != nil {
|
if params.AnchorMessage != nil {
|
||||||
|
|
@ -509,31 +465,19 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
||||||
anchorID = parsedID.ID
|
anchorID = parsedID.ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var hasMore bool
|
|
||||||
if !params.Forward && wa.Main.Config.HistorySync.BackwardsOnDemand {
|
|
||||||
hasMore = conv != nil && ptr.Val(conv.EndOfHistoryTransferType) == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
|
|
||||||
}
|
|
||||||
messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1)
|
messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load messages from database: %w", err)
|
return nil, fmt.Errorf("failed to load messages from database: %w", err)
|
||||||
} else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) {
|
} else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) {
|
||||||
wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0)
|
|
||||||
if hasMore && !params.AllowSlowFetch {
|
|
||||||
return &bridgev2.FetchMessagesResponse{
|
|
||||||
MoreRequiresSlowFetch: true,
|
|
||||||
HasMore: true,
|
|
||||||
Forward: params.Forward,
|
|
||||||
}, nil
|
|
||||||
} else if hasMore {
|
|
||||||
return wa.fetchMessagesFromPhone(ctx, params)
|
|
||||||
}
|
|
||||||
return &bridgev2.FetchMessagesResponse{
|
return &bridgev2.FetchMessagesResponse{
|
||||||
HasMore: false,
|
HasMore: false,
|
||||||
Forward: params.Forward,
|
Forward: params.Forward,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
hasMore := false
|
||||||
|
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
||||||
|
newestTS := messages[0].GetMessageTimestamp()
|
||||||
if len(messages) > params.Count {
|
if len(messages) > params.Count {
|
||||||
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
|
||||||
hasMore = true
|
hasMore = true
|
||||||
// For safety, cut off messages with the oldest timestamp in the response.
|
// For safety, cut off messages with the oldest timestamp in the response.
|
||||||
// Otherwise, if there are multiple messages with the same timestamp, the next fetch may miss some.
|
// Otherwise, if there are multiple messages with the same timestamp, the next fetch may miss some.
|
||||||
|
|
@ -544,78 +488,19 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true)
|
convertedMessages := make([]*bridgev2.BackfillMessage, len(messages))
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to convert messages: %w", err)
|
|
||||||
}
|
|
||||||
resp.HasMore = hasMore
|
|
||||||
resp.Forward = params.Forward
|
|
||||||
resp.MarkRead = markRead
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) {
|
|
||||||
var err error
|
|
||||||
var rows int64
|
|
||||||
if (newestTS == 0 && oldestTS == 0) || !wa.Main.Bridge.Config.Backfill.Queue.AnyEnabled() {
|
|
||||||
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
|
|
||||||
rows, err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
|
|
||||||
} else {
|
|
||||||
// Otherwise just delete the messages that got backfilled
|
|
||||||
rows, err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
zerolog.Ctx(ctx).Warn().Err(err).
|
|
||||||
Stringer("portal_jid", portalJID).
|
|
||||||
Uint64("newest_ts", newestTS).
|
|
||||||
Uint64("oldest_ts", oldestTS).
|
|
||||||
Msg("Failed to delete messages from database after backfill")
|
|
||||||
} else {
|
|
||||||
zerolog.Ctx(ctx).Debug().
|
|
||||||
Stringer("portal_jid", portalJID).
|
|
||||||
Uint64("newest_ts", newestTS).
|
|
||||||
Uint64("oldest_ts", oldestTS).
|
|
||||||
Int64("rows_affected", rows).
|
|
||||||
Msg("Deleted history sync messages from database")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppClient) convertHistorySyncMessages(
|
|
||||||
ctx context.Context,
|
|
||||||
portal *bridgev2.Portal,
|
|
||||||
portalJID types.JID,
|
|
||||||
messages []*waWeb.WebMessageInfo,
|
|
||||||
explodeOnError bool,
|
|
||||||
) (*bridgev2.FetchMessagesResponse, error) {
|
|
||||||
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
|
||||||
newestTS := messages[0].GetMessageTimestamp()
|
|
||||||
convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages))
|
|
||||||
var mediaRequests []*wadb.MediaRequest
|
var mediaRequests []*wadb.MediaRequest
|
||||||
for i, msg := range messages {
|
for i, msg := range messages {
|
||||||
evt, err := wa.Client.ParseWebMessage(portalJID, msg)
|
evt, err := wa.Client.ParseWebMessage(portalJID, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if explodeOnError {
|
// This should never happen because the info is already parsed once before being stored in the database
|
||||||
// This should never happen because the info is already parsed once before being stored in the database
|
return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err)
|
||||||
return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err)
|
|
||||||
}
|
|
||||||
zerolog.Ctx(ctx).Warn().Err(err).
|
|
||||||
Int("msg_index", i).
|
|
||||||
Str("msg_id", msg.GetKey().GetID()).
|
|
||||||
Uint64("msg_time_seconds", msg.GetMessageTimestamp()).
|
|
||||||
Msg("Dropping historical message due to parse error")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !explodeOnError {
|
|
||||||
msgType := getMessageType(evt.Message)
|
|
||||||
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
var mediaReq *wadb.MediaRequest
|
||||||
isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
|
isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
|
||||||
converted, mediaReq := wa.convertHistorySyncMessage(
|
convertedMessages[i], mediaReq = wa.convertHistorySyncMessage(
|
||||||
ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
|
ctx, params.Portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
|
||||||
)
|
)
|
||||||
convertedMessages = append(convertedMessages, converted)
|
|
||||||
if mediaReq != nil {
|
if mediaReq != nil {
|
||||||
mediaRequests = append(mediaRequests, mediaReq)
|
mediaRequests = append(mediaRequests, mediaReq)
|
||||||
}
|
}
|
||||||
|
|
@ -624,10 +509,24 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
|
||||||
return &bridgev2.FetchMessagesResponse{
|
return &bridgev2.FetchMessagesResponse{
|
||||||
Messages: convertedMessages,
|
Messages: convertedMessages,
|
||||||
Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)),
|
Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)),
|
||||||
|
HasMore: hasMore,
|
||||||
|
Forward: endTime == nil,
|
||||||
|
MarkRead: markRead,
|
||||||
|
// TODO set remaining or total count
|
||||||
CompleteCallback: func() {
|
CompleteCallback: func() {
|
||||||
// TODO this only deletes after backfilling. If there's no need for backfill after a relogin,
|
// TODO this only deletes after backfilling. If there's no need for backfill after a relogin,
|
||||||
// the messages will be stuck in the database
|
// the messages will be stuck in the database
|
||||||
wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS)
|
var err error
|
||||||
|
if !wa.Main.Bridge.Config.Backfill.Queue.Enabled && !wa.Main.Bridge.Config.Backfill.WillPaginateManually {
|
||||||
|
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
|
||||||
|
err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
|
||||||
|
} else {
|
||||||
|
// Otherwise just delete the messages that got backfilled
|
||||||
|
err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to delete messages from database after backfill")
|
||||||
|
}
|
||||||
if len(mediaRequests) > 0 {
|
if len(mediaRequests) > 0 {
|
||||||
go func(ctx context.Context) {
|
go func(ctx context.Context) {
|
||||||
for _, req := range mediaRequests {
|
for _, req := range mediaRequests {
|
||||||
|
|
@ -645,94 +544,6 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, params bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) {
|
|
||||||
if params.AnchorMessage == nil {
|
|
||||||
return nil, fmt.Errorf("anchor message is required to fetch messages from phone")
|
|
||||||
}
|
|
||||||
parsed, err := waid.ParseMessageID(params.AnchorMessage.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse anchor message ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msgID := wa.Client.GenerateMessageID()
|
|
||||||
reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{
|
|
||||||
MessageSource: types.MessageSource{
|
|
||||||
Chat: parsed.Chat,
|
|
||||||
Sender: parsed.Sender,
|
|
||||||
IsFromMe: parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.Device.GetLID().ToNonAD(),
|
|
||||||
IsGroup: parsed.Chat.Server == types.GroupServer,
|
|
||||||
},
|
|
||||||
ID: parsed.ID,
|
|
||||||
Timestamp: params.AnchorMessage.Timestamp,
|
|
||||||
}, 50)
|
|
||||||
zerolog.Ctx(ctx).Debug().
|
|
||||||
Str("request_msg_id", msgID).
|
|
||||||
Any("anchor_msg_parsed", parsed).
|
|
||||||
Any("request_data", reqData).
|
|
||||||
Msg("Sending history sync request")
|
|
||||||
_, err = wa.Client.SendMessage(ctx, wa.JID.ToNonAD(), reqData, whatsmeow.SendRequestExtra{
|
|
||||||
ID: msgID,
|
|
||||||
Peer: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to send history sync request: %w", err)
|
|
||||||
}
|
|
||||||
return &bridgev2.FetchMessagesResponse{
|
|
||||||
HasMore: true,
|
|
||||||
Pending: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *waHistorySync.HistorySync) {
|
|
||||||
if len(blob.GetConversations()) > 1 {
|
|
||||||
zerolog.Ctx(ctx).Warn().
|
|
||||||
Int("conversation_count", len(blob.GetConversations())).
|
|
||||||
Msg("Received on-demand history sync with multiple conversations")
|
|
||||||
}
|
|
||||||
for _, conv := range blob.GetConversations() {
|
|
||||||
portalJID, err := types.ParseJID(conv.GetID())
|
|
||||||
if err != nil {
|
|
||||||
zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(portalJID))
|
|
||||||
if err != nil {
|
|
||||||
zerolog.Ctx(ctx).Err(err).Stringer("portal_jid", portalJID).Msg("Failed to get portal for on-demand history sync")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ctx := zerolog.Ctx(ctx).With().
|
|
||||||
Str("portal_id", string(portal.ID)).
|
|
||||||
Str("portal_receiver", string(portal.Receiver)).
|
|
||||||
Stringer("portal_mxid", portal.MXID).
|
|
||||||
Logger().WithContext(ctx)
|
|
||||||
portal.HandleRemoteBackfill(ctx, wa.UserLogin, &simplevent.Backfill{
|
|
||||||
EventMeta: simplevent.EventMeta{
|
|
||||||
Type: bridgev2.RemoteEventBackfill,
|
|
||||||
PortalKey: portal.PortalKey,
|
|
||||||
},
|
|
||||||
GetDataFunc: func(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.FetchMessagesResponse, error) {
|
|
||||||
if len(conv.GetMessages()) == 0 {
|
|
||||||
return &bridgev2.FetchMessagesResponse{}, nil
|
|
||||||
}
|
|
||||||
messages := make([]*waWeb.WebMessageInfo, len(conv.GetMessages()))
|
|
||||||
for i, rawMsg := range conv.GetMessages() {
|
|
||||||
messages[i] = rawMsg.Message
|
|
||||||
}
|
|
||||||
zerolog.Ctx(ctx).Debug().
|
|
||||||
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)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.HasMore = conv.GetEndOfHistoryTransferType() == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
|
|
||||||
return resp, nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppClient) convertHistorySyncMessage(
|
func (wa *WhatsAppClient) convertHistorySyncMessage(
|
||||||
ctx context.Context, 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) {
|
) (*bridgev2.BackfillMessage, *wadb.MediaRequest) {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
|
||||||
AggressiveUpdateInfo: true,
|
AggressiveUpdateInfo: true,
|
||||||
ImplicitReadReceipts: true,
|
ImplicitReadReceipts: true,
|
||||||
Provisioning: bridgev2.ProvisioningCapabilities{
|
Provisioning: bridgev2.ProvisioningCapabilities{
|
||||||
ImagePackImport: true,
|
|
||||||
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
|
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
|
||||||
CreateDM: true,
|
CreateDM: true,
|
||||||
LookupPhone: true,
|
LookupPhone: true,
|
||||||
|
|
@ -52,7 +51,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) {
|
func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) {
|
||||||
return 1, 8
|
return 1, 7
|
||||||
}
|
}
|
||||||
|
|
||||||
const WAMaxFileSize = 2000 * 1024 * 1024
|
const WAMaxFileSize = 2000 * 1024 * 1024
|
||||||
|
|
@ -67,7 +66,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func capID() string {
|
func capID() string {
|
||||||
base := "fi.mau.whatsapp.capabilities.2026_05_12"
|
base := "fi.mau.whatsapp.capabilities.2025_12_15"
|
||||||
if ffmpeg.Supported() {
|
if ffmpeg.Supported() {
|
||||||
return base + "+ffmpeg"
|
return base + "+ffmpeg"
|
||||||
}
|
}
|
||||||
|
|
@ -126,10 +125,10 @@ var whatsappCaps = &event.RoomFeatures{
|
||||||
event.CapMsgSticker: {
|
event.CapMsgSticker: {
|
||||||
MimeTypes: map[string]event.CapabilitySupportLevel{
|
MimeTypes: map[string]event.CapabilitySupportLevel{
|
||||||
"image/webp": event.CapLevelFullySupported,
|
"image/webp": event.CapLevelFullySupported,
|
||||||
|
// TODO see if sending lottie is possible
|
||||||
|
//"video/lottie+json": event.CapLevelFullySupported,
|
||||||
"image/png": event.CapLevelPartialSupport,
|
"image/png": event.CapLevelPartialSupport,
|
||||||
"image/jpeg": event.CapLevelPartialSupport,
|
"image/jpeg": event.CapLevelPartialSupport,
|
||||||
// This will only be accepted if it was imported from WhatsApp
|
|
||||||
"video/lottie+json": event.CapLevelPartialSupport,
|
|
||||||
},
|
},
|
||||||
Caption: event.CapLevelDropped,
|
Caption: event.CapLevelDropped,
|
||||||
MaxSize: WAMaxFileSize,
|
MaxSize: WAMaxFileSize,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ import (
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||||
"maunium.net/go/mautrix/bridgev2/status"
|
"maunium.net/go/mautrix/bridgev2/status"
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||||
)
|
)
|
||||||
|
|
@ -129,7 +128,6 @@ var (
|
||||||
_ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil)
|
_ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil)
|
||||||
_ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil)
|
_ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil)
|
||||||
_ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil)
|
_ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil)
|
||||||
_ bridgev2.StickerImportingNetworkAPI = (*WhatsAppClient)(nil)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var pushCfg = &bridgev2.PushConfig{
|
var pushCfg = &bridgev2.PushConfig{
|
||||||
|
|
@ -469,12 +467,3 @@ func (wa *WhatsAppClient) updatePresence(ctx context.Context, presence types.Pre
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wa *WhatsAppClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
|
|
||||||
return wa.Main.MsgConv.DownloadImagePack(ctx, wa.UserLogin.ID, wa.Client, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) {
|
|
||||||
// TODO
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,6 @@ type Config struct {
|
||||||
RequestLocalTime int `yaml:"request_local_time"`
|
RequestLocalTime int `yaml:"request_local_time"`
|
||||||
MaxAsyncHandle int64 `yaml:"max_async_handle"`
|
MaxAsyncHandle int64 `yaml:"max_async_handle"`
|
||||||
} `yaml:"media_requests"`
|
} `yaml:"media_requests"`
|
||||||
|
|
||||||
BackwardsOnDemand bool `yaml:"backwards_on_demand"`
|
|
||||||
} `yaml:"history_sync"`
|
} `yaml:"history_sync"`
|
||||||
|
|
||||||
displaynameTemplate *template.Template `yaml:"-"`
|
displaynameTemplate *template.Template `yaml:"-"`
|
||||||
|
|
@ -136,7 +134,6 @@ func upgradeConfig(helper up.Helper) {
|
||||||
helper.Copy(up.Str, "history_sync", "media_requests", "request_method")
|
helper.Copy(up.Str, "history_sync", "media_requests", "request_method")
|
||||||
helper.Copy(up.Int, "history_sync", "media_requests", "request_local_time")
|
helper.Copy(up.Int, "history_sync", "media_requests", "request_local_time")
|
||||||
helper.Copy(up.Int, "history_sync", "media_requests", "max_async_handle")
|
helper.Copy(up.Int, "history_sync", "media_requests", "max_async_handle")
|
||||||
helper.Copy(up.Bool, "history_sync", "backwards_on_demand")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DisplaynameParams struct {
|
type DisplaynameParams struct {
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,6 @@ func (wa *WhatsAppConnector) Download(ctx context.Context, mediaID networkid.Med
|
||||||
return wa.downloadMessageDirectMedia(ctx, parsedID, params)
|
return wa.downloadMessageDirectMedia(ctx, parsedID, params)
|
||||||
} else if parsedID.Avatar != nil {
|
} else if parsedID.Avatar != nil {
|
||||||
return wa.downloadAvatarDirectMedia(ctx, parsedID, params)
|
return wa.downloadAvatarDirectMedia(ctx, parsedID, params)
|
||||||
} else if parsedID.Sticker != nil {
|
|
||||||
return wa.downloadStickerDirectMedia(ctx, parsedID, params)
|
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("unexpected media ID parsing result")
|
return nil, fmt.Errorf("unexpected media ID parsing result")
|
||||||
}
|
}
|
||||||
|
|
@ -137,25 +135,8 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
|
|
||||||
ul := wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin)
|
|
||||||
if ul == nil {
|
|
||||||
return nil, fmt.Errorf("%w: user login %s not found", bridgev2.ErrNotLoggedIn, parsedID.UserLogin)
|
|
||||||
}
|
|
||||||
waClient := ul.Client.(*WhatsAppClient)
|
|
||||||
if waClient.Client == nil {
|
|
||||||
return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin)
|
|
||||||
}
|
|
||||||
sticker, err := wa.MsgConv.GetCachedSticker(ctx, waClient.Client, parsedID.Sticker.PackID, parsedID.Sticker.FileHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if sticker == nil {
|
|
||||||
return nil, mautrix.MNotFound.WithMessage("Sticker not found in pack")
|
|
||||||
}
|
|
||||||
return wa.makeDirectMediaResponse(ctx, waClient, sticker, sticker.MimeType, "", nil, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
|
func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
|
||||||
|
log := zerolog.Ctx(ctx)
|
||||||
msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String())
|
msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get message: %w", err)
|
return nil, fmt.Errorf("failed to get message: %w", err)
|
||||||
|
|
@ -193,29 +174,16 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
|
||||||
if waClient.Client == nil {
|
if waClient.Client == nil {
|
||||||
return nil, fmt.Errorf("no WhatsApp client found on login")
|
return nil, fmt.Errorf("no WhatsApp client found on login")
|
||||||
}
|
}
|
||||||
return wa.makeDirectMediaResponse(ctx, waClient, keys, keys.MimeType, msg.ID, keys, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppConnector) makeDirectMediaResponse(
|
|
||||||
ctx context.Context,
|
|
||||||
waClient *WhatsAppClient,
|
|
||||||
dm whatsmeow.DownloadableMessage,
|
|
||||||
mimeType string,
|
|
||||||
msgID networkid.MessageID,
|
|
||||||
keys *msgconv.FailedMediaKeys,
|
|
||||||
params map[string]string,
|
|
||||||
) (mediaproxy.GetMediaResponse, error) {
|
|
||||||
return &mediaproxy.GetMediaResponseFile{
|
return &mediaproxy.GetMediaResponseFile{
|
||||||
Callback: func(f *os.File) (*mediaproxy.FileMeta, error) {
|
Callback: func(f *os.File) (*mediaproxy.FileMeta, error) {
|
||||||
log := zerolog.Ctx(ctx)
|
err := waClient.Client.DownloadToFile(ctx, keys, f)
|
||||||
err := waClient.Client.DownloadToFile(ctx, dm, f)
|
if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent) {
|
||||||
if keys != nil && (errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent)) {
|
|
||||||
val := params["fi.mau.whatsapp.reload_media"]
|
val := params["fi.mau.whatsapp.reload_media"]
|
||||||
if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") {
|
if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") {
|
||||||
return nil, ErrReloadNeeded
|
return nil, ErrReloadNeeded
|
||||||
}
|
}
|
||||||
log.Trace().Msg("Media not found for direct download, requesting and waiting")
|
log.Trace().Msg("Media not found for direct download, requesting and waiting")
|
||||||
err = waClient.requestAndWaitDirectMedia(ctx, msgID, keys)
|
err = waClient.requestAndWaitDirectMedia(ctx, msg.ID, keys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Trace().Err(err).Msg("Failed to wait for media for direct download")
|
log.Trace().Err(err).Msg("Failed to wait for media for direct download")
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -229,23 +197,24 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if mimeType == "application/was" {
|
mime := keys.MimeType
|
||||||
|
if mime == "application/was" {
|
||||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||||
return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err)
|
return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err)
|
||||||
} else if zipData, err := io.ReadAll(f); err != nil {
|
} else if zipData, err := io.ReadAll(f); err != nil {
|
||||||
return nil, fmt.Errorf("failed to read sticker zip: %w", err)
|
return nil, fmt.Errorf("failed to read sticker zip: %w", err)
|
||||||
} else if data, _, err := msgconv.ExtractAnimatedSticker(zipData); err != nil {
|
} else if data, err := msgconv.ExtractAnimatedSticker(zipData); err != nil {
|
||||||
return nil, fmt.Errorf("failed to extract animated sticker: %w %x", err, zipData)
|
return nil, fmt.Errorf("failed to extract animated sticker: %w %x", err, zipData)
|
||||||
} else if _, err := f.WriteAt(data, 0); err != nil {
|
} else if _, err := f.WriteAt(data, 0); err != nil {
|
||||||
return nil, fmt.Errorf("failed to write animated sticker to file: %w", err)
|
return nil, fmt.Errorf("failed to write animated sticker to file: %w", err)
|
||||||
} else if err := f.Truncate(int64(len(data))); err != nil {
|
} else if err := f.Truncate(int64(len(data))); err != nil {
|
||||||
return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err)
|
return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err)
|
||||||
}
|
}
|
||||||
mimeType = "video/lottie+json"
|
mime = "video/lottie+json"
|
||||||
}
|
}
|
||||||
|
|
||||||
return &mediaproxy.FileMeta{
|
return &mediaproxy.FileMeta{
|
||||||
ContentType: mimeType,
|
ContentType: mime,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,3 @@ history_sync:
|
||||||
request_local_time: 120
|
request_local_time: 120
|
||||||
# Maximum number of media request responses to handle in parallel per user.
|
# Maximum number of media request responses to handle in parallel per user.
|
||||||
max_async_handle: 2
|
max_async_handle: 2
|
||||||
# Use on-demand history sync requests for fetching older messages?
|
|
||||||
# This only applies when using the backfill queue, never for forward backfills.
|
|
||||||
backwards_on_demand: false
|
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,6 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
|
||||||
wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID)
|
wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID)
|
||||||
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
|
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
|
||||||
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2))
|
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2))
|
||||||
zerolog.Ctx(ctx).Trace().Any("payload", waMsg).Msg("Outgoing message payload")
|
|
||||||
resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req)
|
resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -392,10 +391,6 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
|
func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
|
||||||
if msg.Type.IsSelf && msg.OrigSender != nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -309,55 +309,20 @@ func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string,
|
||||||
|
|
||||||
func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) {
|
func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) {
|
||||||
success = true
|
success = true
|
||||||
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
parsedMessageType := getMessageType(evt.Message)
|
|
||||||
if encReact := evt.Message.GetEncReactionMessage(); encReact != nil {
|
|
||||||
decrypted, err := wa.Client.DecryptReaction(ctx, evt)
|
|
||||||
if err != nil {
|
|
||||||
wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt reaction")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
decrypted.Key = encReact.GetTargetMessageKey()
|
|
||||||
evt.Message.ReactionMessage = decrypted
|
|
||||||
}
|
|
||||||
if encComment := evt.Message.GetEncCommentMessage(); encComment != nil {
|
|
||||||
decrypted, err := wa.Client.DecryptComment(ctx, evt)
|
|
||||||
if err != nil {
|
|
||||||
wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt comment")
|
|
||||||
} else {
|
|
||||||
decrypted.EncCommentMessage = evt.Message.GetEncCommentMessage()
|
|
||||||
evt.Message = decrypted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if encMessage := evt.Message.GetSecretEncryptedMessage(); encMessage != nil {
|
|
||||||
decrypted, err := wa.Client.DecryptSecretEncryptedMessage(ctx, evt)
|
|
||||||
if err != nil {
|
|
||||||
wa.UserLogin.Log.Err(err).
|
|
||||||
Str("message_id", evt.Info.ID).
|
|
||||||
Stringer("evt_sender", evt.Info.Sender).
|
|
||||||
Any("target_message_key", encMessage.TargetMessageKey).
|
|
||||||
Msg("Failed to decrypt secret-encrypted message")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
evt.RawMessage = decrypted
|
|
||||||
evt.UnwrapRaw()
|
|
||||||
parsedMessageType = getMessageType(evt.Message)
|
|
||||||
}
|
|
||||||
wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID)
|
wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID)
|
||||||
wa.UserLogin.Log.Trace().
|
wa.UserLogin.Log.Trace().
|
||||||
Any("info", evt.Info).
|
Any("info", evt.Info).
|
||||||
Any("payload", evt.Message).
|
Any("payload", evt.Message).
|
||||||
Msg("Received WhatsApp message")
|
Msg("Received WhatsApp message")
|
||||||
|
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
|
||||||
|
return
|
||||||
|
}
|
||||||
if evt.Info.IsFromMe &&
|
if evt.Info.IsFromMe &&
|
||||||
evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil &&
|
evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil &&
|
||||||
wa.Main.Bridge.Config.Backfill.Enabled {
|
wa.Main.Bridge.Config.Backfill.Enabled &&
|
||||||
|
wa.Client.ManualHistorySyncDownload {
|
||||||
wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification)
|
wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification)
|
||||||
}
|
}
|
||||||
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation()
|
messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation()
|
||||||
if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD {
|
if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD {
|
||||||
|
|
@ -386,6 +351,38 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsedMessageType := getMessageType(evt.Message)
|
||||||
|
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if encReact := evt.Message.GetEncReactionMessage(); encReact != nil {
|
||||||
|
decrypted, err := wa.Client.DecryptReaction(ctx, evt)
|
||||||
|
if err != nil {
|
||||||
|
wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt reaction")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decrypted.Key = encReact.GetTargetMessageKey()
|
||||||
|
evt.Message.ReactionMessage = decrypted
|
||||||
|
}
|
||||||
|
if encComment := evt.Message.GetEncCommentMessage(); encComment != nil {
|
||||||
|
decrypted, err := wa.Client.DecryptComment(ctx, evt)
|
||||||
|
if err != nil {
|
||||||
|
wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt comment")
|
||||||
|
} else {
|
||||||
|
decrypted.EncCommentMessage = evt.Message.GetEncCommentMessage()
|
||||||
|
evt.Message = decrypted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if encMessage := evt.Message.GetSecretEncryptedMessage(); encMessage != nil {
|
||||||
|
decrypted, err := wa.Client.DecryptSecretEncryptedMessage(ctx, evt)
|
||||||
|
if err != nil {
|
||||||
|
wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
evt.RawMessage = decrypted
|
||||||
|
evt.UnwrapRaw()
|
||||||
|
parsedMessageType = getMessageType(evt.Message)
|
||||||
|
}
|
||||||
res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{
|
res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{
|
||||||
MessageInfoWrapper: &MessageInfoWrapper{
|
MessageInfoWrapper: &MessageInfoWrapper{
|
||||||
Info: evt.Info,
|
Info: evt.Info,
|
||||||
|
|
|
||||||
|
|
@ -116,12 +116,9 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo
|
||||||
AsList()
|
AsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) (int64, error) {
|
func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) error {
|
||||||
res, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after)
|
_, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after)
|
||||||
if err != nil {
|
return err
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return res.RowsAffected()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLoginID) error {
|
func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLoginID) error {
|
||||||
|
|
@ -129,12 +126,9 @@ func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLog
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (int64, error) {
|
func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) error {
|
||||||
res, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID)
|
_, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID)
|
||||||
if err != nil {
|
return err
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return res.RowsAffected()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mq *MessageQuery) ConversationHasMessages(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (exists bool, err error) {
|
func (mq *MessageQuery) ConversationHasMessages(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (exists bool, err error) {
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,8 @@ func getMessageType(waMsg *waE2E.Message) string {
|
||||||
return "poll result snapshot"
|
return "poll result snapshot"
|
||||||
case waMsg.MessageHistoryBundle != nil:
|
case waMsg.MessageHistoryBundle != nil:
|
||||||
return "message history bundle"
|
return "message history bundle"
|
||||||
|
case waMsg.MessageHistoryNotice != nil:
|
||||||
|
return "message history notice"
|
||||||
case waMsg.RequestPhoneNumberMessage != nil:
|
case waMsg.RequestPhoneNumberMessage != nil:
|
||||||
return "request phone number"
|
return "request phone number"
|
||||||
case waMsg.KeepInChatMessage != nil:
|
case waMsg.KeepInChatMessage != nil:
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ package msgconv
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -202,7 +201,6 @@ func (mc *MessageConverter) constructMediaMessage(
|
||||||
FileSHA256: uploaded.FileSHA256,
|
FileSHA256: uploaded.FileSHA256,
|
||||||
FileLength: proto.Uint64(uploaded.FileLength),
|
FileLength: proto.Uint64(uploaded.FileLength),
|
||||||
URL: proto.String(uploaded.URL),
|
URL: proto.String(uploaded.URL),
|
||||||
IsLottie: proto.Bool(mime == "application/was"),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case event.MsgAudio:
|
case event.MsgAudio:
|
||||||
|
|
@ -484,17 +482,6 @@ func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) {
|
||||||
return webpBuffer.Bytes(), size, nil
|
return webpBuffer.Bytes(), size, 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
|
|
||||||
}
|
|
||||||
fileHash, err := base64.StdEncoding.DecodeString(info.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return mc.GetCachedSticker(ctx, getClient(ctx), strings.TrimPrefix(info.PackURL, StickerPackURLPrefix), fileHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MessageConverter) reuploadFileToWhatsApp(
|
func (mc *MessageConverter) reuploadFileToWhatsApp(
|
||||||
ctx context.Context, content *event.MessageEventContent,
|
ctx context.Context, content *event.MessageEventContent,
|
||||||
) (*whatsmeow.UploadResponse, []byte, string, error) {
|
) (*whatsmeow.UploadResponse, []byte, string, error) {
|
||||||
|
|
@ -503,25 +490,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
|
||||||
if content.FileName != "" {
|
if content.FileName != "" {
|
||||||
fileName = content.FileName
|
fileName = content.FileName
|
||||||
}
|
}
|
||||||
var data []byte
|
data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
||||||
var err error
|
|
||||||
var sticker *types.StickerPackItem
|
|
||||||
if sticker, err = mc.getOriginalBridgedSticker(ctx, content.Info.BridgedSticker); err != nil {
|
|
||||||
zerolog.Ctx(ctx).Warn().Err(err).
|
|
||||||
Msg("Failed to get original bridged sticker, falling back to downloading from URL")
|
|
||||||
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
|
||||||
} else if sticker != nil {
|
|
||||||
if sticker.MimeType == "application/was" {
|
|
||||||
data, err = getClient(ctx).Download(ctx, sticker)
|
|
||||||
mime = sticker.MimeType
|
|
||||||
} else {
|
|
||||||
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
|
||||||
}
|
|
||||||
content.Info.Width = sticker.Width
|
|
||||||
content.Info.Height = sticker.Height
|
|
||||||
} else {
|
|
||||||
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, "", fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
|
return nil, nil, "", fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
|
||||||
}
|
}
|
||||||
|
|
@ -539,14 +508,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
|
||||||
case event.MessageType(event.EventSticker.Type):
|
case event.MessageType(event.EventSticker.Type):
|
||||||
isSticker = true
|
isSticker = true
|
||||||
mediaType = whatsmeow.MediaImage
|
mediaType = whatsmeow.MediaImage
|
||||||
if mime == "video/lottie+json" {
|
if mime != "image/webp" || content.Info.Width != content.Info.Height {
|
||||||
// This likely won't work
|
|
||||||
data, err = PackAnimatedSticker(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, mime, fmt.Errorf("%w (packing animated sticker): %w", bridgev2.ErrMediaConvertFailed, err)
|
|
||||||
}
|
|
||||||
mime = "application/was"
|
|
||||||
} else if (mime != "image/webp" || content.Info.Width != content.Info.Height) && mime != "application/was" {
|
|
||||||
var size int
|
var size int
|
||||||
data, size, err = mc.convertToWebP(data)
|
data, size, err = mc.convertToWebP(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,10 @@ func (mc *MessageConverter) ToMatrix(
|
||||||
part, contextInfo = mc.convertGroupInviteMessage(ctx, info, waMsg.GroupInviteMessage)
|
part, contextInfo = mc.convertGroupInviteMessage(ctx, info, waMsg.GroupInviteMessage)
|
||||||
case waMsg.ProtocolMessage != nil && waMsg.ProtocolMessage.GetType() == waE2E.ProtocolMessage_EPHEMERAL_SETTING:
|
case waMsg.ProtocolMessage != nil && waMsg.ProtocolMessage.GetType() == waE2E.ProtocolMessage_EPHEMERAL_SETTING:
|
||||||
part, contextInfo = mc.convertEphemeralSettingMessage(ctx, waMsg.ProtocolMessage, info.Timestamp, isBackfill)
|
part, contextInfo = mc.convertEphemeralSettingMessage(ctx, waMsg.ProtocolMessage, info.Timestamp, isBackfill)
|
||||||
|
case waMsg.MessageHistoryBundle != nil:
|
||||||
|
part, contextInfo = mc.convertMessageHistoryShare(ctx, info, waMsg.MessageHistoryBundle.GetMessageHistoryMetadata(), waMsg.MessageHistoryBundle.GetContextInfo())
|
||||||
|
case waMsg.MessageHistoryNotice != nil:
|
||||||
|
part, contextInfo = mc.convertMessageHistoryShare(ctx, info, waMsg.MessageHistoryNotice.GetMessageHistoryMetadata(), waMsg.MessageHistoryNotice.GetContextInfo())
|
||||||
case waMsg.EncCommentMessage != nil:
|
case waMsg.EncCommentMessage != nil:
|
||||||
part = failedCommentPart
|
part = failedCommentPart
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,6 @@
|
||||||
package msgconv
|
package msgconv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
|
||||||
|
|
||||||
"go.mau.fi/whatsmeow/types"
|
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
"maunium.net/go/mautrix/format"
|
"maunium.net/go/mautrix/format"
|
||||||
|
|
||||||
|
|
@ -46,16 +43,12 @@ type MessageConverter struct {
|
||||||
DisableViewOnce bool
|
DisableViewOnce bool
|
||||||
DirectMedia bool
|
DirectMedia bool
|
||||||
OldMediaSuffix string
|
OldMediaSuffix string
|
||||||
|
|
||||||
stickerPackCache map[string]*types.StickerPack
|
|
||||||
stickerPackCacheLock sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(br *bridgev2.Bridge) *MessageConverter {
|
func New(br *bridgev2.Bridge) *MessageConverter {
|
||||||
mc := &MessageConverter{
|
mc := &MessageConverter{
|
||||||
Bridge: br,
|
Bridge: br,
|
||||||
MaxFileSize: 50 * 1024 * 1024,
|
MaxFileSize: 50 * 1024 * 1024,
|
||||||
stickerPackCache: make(map[string]*types.StickerPack),
|
|
||||||
}
|
}
|
||||||
mc.HTMLParser = &format.HTMLParser{
|
mc.HTMLParser = &format.HTMLParser{
|
||||||
PillConverter: mc.convertPill,
|
PillConverter: mc.convertPill,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
package msgconv
|
package msgconv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
@ -24,18 +26,21 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"go.mau.fi/util/exmime"
|
"go.mau.fi/util/exmime"
|
||||||
"go.mau.fi/util/exslices"
|
"go.mau.fi/util/exslices"
|
||||||
|
"go.mau.fi/util/lottie"
|
||||||
|
"go.mau.fi/util/random"
|
||||||
"go.mau.fi/whatsmeow"
|
"go.mau.fi/whatsmeow"
|
||||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||||
"go.mau.fi/whatsmeow/types"
|
"go.mau.fi/whatsmeow/types"
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
"maunium.net/go/mautrix/bridgev2/database"
|
"maunium.net/go/mautrix/bridgev2/database"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||||
)
|
)
|
||||||
|
|
@ -82,11 +87,11 @@ func (mc *MessageConverter) convertMediaMessage(
|
||||||
MimeType: msg.GetMimetype(),
|
MimeType: msg.GetMimetype(),
|
||||||
}
|
}
|
||||||
if mc.DirectMedia {
|
if mc.DirectMedia {
|
||||||
|
preparedMedia.FillFileName()
|
||||||
if preparedMedia.Info.MimeType == "application/was" {
|
if preparedMedia.Info.MimeType == "application/was" {
|
||||||
preparedMedia.Info.MimeType = "video/lottie+json"
|
preparedMedia.Info.MimeType = "video/lottie+json"
|
||||||
preparedMedia.FileName = "sticker.json"
|
preparedMedia.FileName = "sticker.json"
|
||||||
}
|
}
|
||||||
preparedMedia.FillFileName()
|
|
||||||
var err error
|
var err error
|
||||||
portal := getPortal(ctx)
|
portal := getPortal(ctx)
|
||||||
idOverride := getEditTargetID(ctx)
|
idOverride := getEditTargetID(ctx)
|
||||||
|
|
@ -193,9 +198,7 @@ type PreparedMedia struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *PreparedMedia) FillFileName() *PreparedMedia {
|
func (pm *PreparedMedia) FillFileName() *PreparedMedia {
|
||||||
if pm.Type == event.EventSticker {
|
if pm.FileName == "" {
|
||||||
pm.FileName = ""
|
|
||||||
} else if pm.FileName == "" {
|
|
||||||
pm.FileName = strings.TrimPrefix(string(pm.MsgType), "m.") + exmime.ExtensionFromMimetype(pm.Info.MimeType)
|
pm.FileName = strings.TrimPrefix(string(pm.MsgType), "m.") + exmime.ExtensionFromMimetype(pm.Info.MimeType)
|
||||||
}
|
}
|
||||||
return pm
|
return pm
|
||||||
|
|
@ -236,19 +239,6 @@ type MediaMessageWithDuration interface {
|
||||||
|
|
||||||
const WhatsAppStickerSize = 190
|
const WhatsAppStickerSize = 190
|
||||||
|
|
||||||
func fixStickerDimensions(info *event.FileInfo) {
|
|
||||||
if info.Width == info.Height {
|
|
||||||
info.Width = WhatsAppStickerSize
|
|
||||||
info.Height = WhatsAppStickerSize
|
|
||||||
} else if info.Width > info.Height {
|
|
||||||
info.Height /= info.Width / WhatsAppStickerSize
|
|
||||||
info.Width = WhatsAppStickerSize
|
|
||||||
} else {
|
|
||||||
info.Width /= info.Height / WhatsAppStickerSize
|
|
||||||
info.Height = WhatsAppStickerSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
|
func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
|
||||||
extraInfo := map[string]any{}
|
extraInfo := map[string]any{}
|
||||||
data := &PreparedMedia{
|
data := &PreparedMedia{
|
||||||
|
|
@ -297,7 +287,19 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
|
||||||
case *waE2E.StickerMessage:
|
case *waE2E.StickerMessage:
|
||||||
data.Type = event.EventSticker
|
data.Type = event.EventSticker
|
||||||
data.FileName = "sticker" + exmime.ExtensionFromMimetype(msg.GetMimetype())
|
data.FileName = "sticker" + exmime.ExtensionFromMimetype(msg.GetMimetype())
|
||||||
fixStickerDimensions(data.Info)
|
if msg.GetMimetype() == "application/was" && data.FileName == "sticker" {
|
||||||
|
data.FileName = "sticker.json"
|
||||||
|
}
|
||||||
|
if data.Info.Width == data.Info.Height {
|
||||||
|
data.Info.Width = WhatsAppStickerSize
|
||||||
|
data.Info.Height = WhatsAppStickerSize
|
||||||
|
} else if data.Info.Width > data.Info.Height {
|
||||||
|
data.Info.Height /= data.Info.Width / WhatsAppStickerSize
|
||||||
|
data.Info.Width = WhatsAppStickerSize
|
||||||
|
} else {
|
||||||
|
data.Info.Width /= data.Info.Height / WhatsAppStickerSize
|
||||||
|
data.Info.Height = WhatsAppStickerSize
|
||||||
|
}
|
||||||
case *waE2E.VideoMessage:
|
case *waE2E.VideoMessage:
|
||||||
data.MsgType = event.MsgVideo
|
data.MsgType = event.MsgVideo
|
||||||
pairedMediaType := msg.GetContextInfo().GetPairedMediaType()
|
pairedMediaType := msg.GetContextInfo().GetPairedMediaType()
|
||||||
|
|
@ -357,15 +359,12 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
||||||
) error {
|
) error {
|
||||||
client := getClient(ctx)
|
client := getClient(ctx)
|
||||||
intent := getIntent(ctx)
|
intent := getIntent(ctx)
|
||||||
var roomID id.RoomID
|
portal := getPortal(ctx)
|
||||||
if portal := getPortal(ctx); portal != nil {
|
|
||||||
roomID = portal.MXID
|
|
||||||
}
|
|
||||||
var thumbnailData []byte
|
var thumbnailData []byte
|
||||||
var thumbnailInfo *event.FileInfo
|
var thumbnailInfo *event.FileInfo
|
||||||
if part.Info.Size > uploadFileThreshold {
|
if part.Info.Size > uploadFileThreshold {
|
||||||
var err error
|
var err error
|
||||||
part.URL, part.File, err = intent.UploadMediaStream(ctx, roomID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
|
part.URL, part.File, err = intent.UploadMediaStream(ctx, portal.MXID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
|
||||||
err := client.DownloadToFile(ctx, message, file.(*os.File))
|
err := client.DownloadToFile(ctx, message, file.(*os.File))
|
||||||
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
|
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
|
||||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
|
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
|
||||||
|
|
@ -398,14 +397,12 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if part.Type == event.EventSticker && part.Info.MimeType == "image/webp" {
|
|
||||||
mc.fillWebPStickerInfo(ctx, part, data)
|
|
||||||
}
|
}
|
||||||
if part.Info.MimeType == "" {
|
if part.Info.MimeType == "" {
|
||||||
part.Info.MimeType = http.DetectContentType(data)
|
part.Info.MimeType = http.DetectContentType(data)
|
||||||
}
|
}
|
||||||
part.FillFileName()
|
part.FillFileName()
|
||||||
part.URL, part.File, err = intent.UploadMedia(ctx, roomID, data, part.FileName, part.Info.MimeType)
|
part.URL, part.File, err = intent.UploadMedia(ctx, portal.MXID, data, part.FileName, part.Info.MimeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err)
|
return fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err)
|
||||||
}
|
}
|
||||||
|
|
@ -414,7 +411,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
||||||
var err error
|
var err error
|
||||||
part.Info.ThumbnailURL, part.Info.ThumbnailFile, err = intent.UploadMedia(
|
part.Info.ThumbnailURL, part.Info.ThumbnailFile, err = intent.UploadMedia(
|
||||||
ctx,
|
ctx,
|
||||||
roomID,
|
portal.MXID,
|
||||||
thumbnailData,
|
thumbnailData,
|
||||||
"thumbnail"+exmime.ExtensionFromMimetype(thumbnailInfo.MimeType),
|
"thumbnail"+exmime.ExtensionFromMimetype(thumbnailInfo.MimeType),
|
||||||
thumbnailInfo.MimeType,
|
thumbnailInfo.MimeType,
|
||||||
|
|
@ -428,6 +425,68 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mc *MessageConverter) extractAnimatedSticker(fileInfo *PreparedMedia, data []byte) ([]byte, error) {
|
||||||
|
data, err := ExtractAnimatedSticker(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fileInfo.Info.MimeType = "video/lottie+json"
|
||||||
|
fileInfo.FileName = "sticker.json"
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *MessageConverter) convertAnimatedSticker(ctx context.Context, fileInfo *PreparedMedia, data []byte) ([]byte, []byte, *event.FileInfo, error) {
|
||||||
|
data, err := mc.extractAnimatedSticker(fileInfo, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
c := mc.AnimatedStickerConfig
|
||||||
|
if c.Target == "disable" {
|
||||||
|
return data, nil, nil, nil
|
||||||
|
} else if !lottie.Supported() {
|
||||||
|
zerolog.Ctx(ctx).Warn().Msg("Animated sticker conversion is enabled, but lottieconverter is not installed")
|
||||||
|
return data, nil, nil, nil
|
||||||
|
}
|
||||||
|
input := bytes.NewReader(data)
|
||||||
|
fileInfo.Info.MimeType = "image/" + c.Target
|
||||||
|
fileInfo.FileName = "sticker." + c.Target
|
||||||
|
switch c.Target {
|
||||||
|
case "png":
|
||||||
|
var output bytes.Buffer
|
||||||
|
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, "1")
|
||||||
|
return output.Bytes(), nil, nil, err
|
||||||
|
case "gif":
|
||||||
|
var output bytes.Buffer
|
||||||
|
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS))
|
||||||
|
return output.Bytes(), nil, nil, err
|
||||||
|
case "webm", "webp":
|
||||||
|
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("mautrix-whatsapp-lottieconverter-%s.%s", random.String(10), c.Target))
|
||||||
|
defer func() {
|
||||||
|
_ = os.Remove(tmpFile)
|
||||||
|
}()
|
||||||
|
thumbnailData, err := lottie.FFmpegConvert(ctx, input, tmpFile, c.Args.Width, c.Args.Height, c.Args.FPS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
data, err = os.ReadFile(tmpFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("failed to read converted file: %w", err)
|
||||||
|
}
|
||||||
|
var thumbnailInfo *event.FileInfo
|
||||||
|
if thumbnailData != nil {
|
||||||
|
thumbnailInfo = &event.FileInfo{
|
||||||
|
MimeType: "image/png",
|
||||||
|
Width: c.Args.Width,
|
||||||
|
Height: c.Args.Height,
|
||||||
|
Size: len(thumbnailData),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, thumbnailData, thumbnailInfo, nil
|
||||||
|
default:
|
||||||
|
return nil, nil, nil, fmt.Errorf("unsupported target format %s", c.Target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *PreparedMedia, keys *FailedMediaKeys, err error) *bridgev2.ConvertedMessagePart {
|
func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *PreparedMedia, keys *FailedMediaKeys, err error) *bridgev2.ConvertedMessagePart {
|
||||||
logLevel := zerolog.ErrorLevel
|
logLevel := zerolog.ErrorLevel
|
||||||
var extra map[string]any
|
var extra map[string]any
|
||||||
|
|
@ -472,3 +531,28 @@ func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *Pre
|
||||||
}
|
}
|
||||||
return part
|
return part
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExtractAnimatedSticker(data []byte) ([]byte, error) {
|
||||||
|
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read sticker zip: %w", err)
|
||||||
|
}
|
||||||
|
animationFile, err := zipReader.Open("animation/animation.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open animation.json: %w", err)
|
||||||
|
}
|
||||||
|
animationFileInfo, err := animationFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
_ = animationFile.Close()
|
||||||
|
return nil, fmt.Errorf("failed to stat animation.json: %w", err)
|
||||||
|
} else if animationFileInfo.Size() > uploadFileThreshold {
|
||||||
|
_ = animationFile.Close()
|
||||||
|
return nil, fmt.Errorf("animation.json is too large (%.2f MiB)", float64(animationFileInfo.Size())/1024/1024)
|
||||||
|
}
|
||||||
|
data, err = io.ReadAll(animationFile)
|
||||||
|
_ = animationFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read animation.json: %w", err)
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,72 @@ func (mc *MessageConverter) convertPlaceholderMessage(ctx context.Context, rawMs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mc *MessageConverter) getHistoryReceiverName(ctx context.Context, receiver string) string {
|
||||||
|
jid, err := types.ParseJID(receiver)
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Str("receiver_jid", receiver).Msg("Failed to parse message history receiver JID")
|
||||||
|
return receiver
|
||||||
|
}
|
||||||
|
_, displayname, err := mc.getBasicUserInfo(ctx, jid)
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Stringer("receiver_jid", jid).Msg("Failed to resolve message history receiver")
|
||||||
|
if jid.User != "" {
|
||||||
|
return jid.User
|
||||||
|
}
|
||||||
|
return receiver
|
||||||
|
}
|
||||||
|
return displayname
|
||||||
|
}
|
||||||
|
|
||||||
|
func messageHistoryStartTime(metadata *waE2E.MessageHistoryMetadata, fallback time.Time) time.Time {
|
||||||
|
if metadata == nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
if ts := metadata.GetOldestMessageTimestamp(); ts > 0 {
|
||||||
|
return time.Unix(ts, 0).Local()
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *MessageConverter) convertMessageHistoryShare(ctx context.Context, info *types.MessageInfo, metadata *waE2E.MessageHistoryMetadata, contextInfo *waE2E.ContextInfo) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||||
|
names := make([]string, 0, len(metadata.GetHistoryReceivers()))
|
||||||
|
for _, receiver := range metadata.GetHistoryReceivers() {
|
||||||
|
if name := mc.getHistoryReceiverName(ctx, receiver); name != "" {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
receivers := strings.Join(names, ", ")
|
||||||
|
var fallback time.Time
|
||||||
|
if info != nil {
|
||||||
|
fallback = info.Timestamp
|
||||||
|
}
|
||||||
|
startAt := messageHistoryStartTime(metadata, fallback)
|
||||||
|
body := "Message history shared."
|
||||||
|
if !startAt.IsZero() {
|
||||||
|
startTime := startAt.Format("Jan 2, 2006 at 3:04 PM")
|
||||||
|
body = fmt.Sprintf("Message history shared starting on %s.", startTime)
|
||||||
|
switch {
|
||||||
|
case info != nil && info.IsFromMe && receivers != "":
|
||||||
|
body = fmt.Sprintf("You sent %s message history that starts on %s.", receivers, startTime)
|
||||||
|
case receivers != "":
|
||||||
|
body = fmt.Sprintf("Sent %s message history that starts on %s.", receivers, startTime)
|
||||||
|
}
|
||||||
|
} else if info != nil && info.IsFromMe && receivers != "" {
|
||||||
|
body = fmt.Sprintf("You sent %s message history.", receivers)
|
||||||
|
} else if receivers != "" {
|
||||||
|
body = fmt.Sprintf("Sent %s message history.", receivers)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &bridgev2.ConvertedMessagePart{
|
||||||
|
Type: event.EventMessage,
|
||||||
|
Content: &event.MessageEventContent{
|
||||||
|
MsgType: event.MsgNotice,
|
||||||
|
Body: body,
|
||||||
|
},
|
||||||
|
}, contextInfo
|
||||||
|
}
|
||||||
|
|
||||||
const inviteMsg = `%s<hr/>This invitation to join "%s" expires at %s. Reply to this message with <code>%s accept</code> to accept the invite.`
|
const inviteMsg = `%s<hr/>This invitation to join "%s" expires at %s. Reply to this message with <code>%s accept</code> to accept the invite.`
|
||||||
const inviteMsgBroken = `%s<hr/>This invitation to join "%s" expires at %s. However, the invite message is broken or unsupported and cannot be accepted.`
|
const inviteMsgBroken = `%s<hr/>This invitation to join "%s" expires at %s. However, the invite message is broken or unsupported and cannot be accepted.`
|
||||||
const GroupInviteMetaField = "fi.mau.whatsapp.invite"
|
const GroupInviteMetaField = "fi.mau.whatsapp.invite"
|
||||||
|
|
|
||||||
|
|
@ -1,455 +0,0 @@
|
||||||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
|
||||||
// Copyright (C) 2026 Tulir Asokan
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package msgconv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/tidwall/gjson"
|
|
||||||
"go.mau.fi/util/exstrings"
|
|
||||||
"go.mau.fi/util/lottie"
|
|
||||||
"go.mau.fi/util/random"
|
|
||||||
"go.mau.fi/whatsmeow"
|
|
||||||
"go.mau.fi/whatsmeow/types"
|
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
|
||||||
"maunium.net/go/mautrix/bridgev2/database"
|
|
||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (mc *MessageConverter) GetCachedStickerPack(ctx context.Context, client *whatsmeow.Client, packID string) (*types.StickerPack, error) {
|
|
||||||
mc.stickerPackCacheLock.Lock()
|
|
||||||
defer mc.stickerPackCacheLock.Unlock()
|
|
||||||
cached, ok := mc.stickerPackCache[packID]
|
|
||||||
if ok {
|
|
||||||
if cached == nil {
|
|
||||||
return nil, bridgev2.RespError(mautrix.MNotFound.WithMessage("sticker pack not found (cached)"))
|
|
||||||
}
|
|
||||||
return cached, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pack, err := client.FetchStickerPack(ctx, packID)
|
|
||||||
if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) {
|
|
||||||
mc.stickerPackCache[packID] = nil
|
|
||||||
return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mc.stickerPackCache[packID] = pack
|
|
||||||
if packID != pack.StickerPackID {
|
|
||||||
mc.stickerPackCache[pack.StickerPackID] = pack
|
|
||||||
}
|
|
||||||
return pack, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MessageConverter) GetCachedSticker(ctx context.Context, client *whatsmeow.Client, packID string, hash []byte) (*types.StickerPackItem, error) {
|
|
||||||
pack, err := mc.GetCachedStickerPack(ctx, client, packID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, sticker := range pack.Stickers {
|
|
||||||
if bytes.Equal(sticker.FileHash, hash) {
|
|
||||||
return sticker, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MessageConverter) DownloadImagePack(ctx context.Context, userLoginID networkid.UserLoginID, client *whatsmeow.Client, inputURL string) (*bridgev2.ImportedImagePack, error) {
|
|
||||||
parsedURL, err := url.Parse(inputURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
|
|
||||||
} else if parsedURL.Host != "api.whatsapp.com" && parsedURL.Host != "wa.me" {
|
|
||||||
return nil, bridgev2.WrapRespErr(fmt.Errorf("invalid host %q", parsedURL.Host), mautrix.MNotFound)
|
|
||||||
} else if !strings.HasPrefix(parsedURL.Path, "/stickerpack/") {
|
|
||||||
return nil, bridgev2.WrapRespErr(fmt.Errorf("invalid path %q", parsedURL.Path), mautrix.MNotFound)
|
|
||||||
}
|
|
||||||
packName := strings.Split(strings.TrimPrefix(parsedURL.Path, "/stickerpack/"), "/")[0]
|
|
||||||
if packName == "" {
|
|
||||||
return nil, bridgev2.WrapRespErr(fmt.Errorf("empty pack name"), mautrix.MNotFound)
|
|
||||||
}
|
|
||||||
pack, err := mc.GetCachedStickerPack(ctx, client, packName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
canonicalURL := "https://wa.me/stickerpack/" + pack.StickerPackID
|
|
||||||
topLevelExtra := map[string]any{
|
|
||||||
"fi.mau.whatsapp.stickerpack": map[string]any{
|
|
||||||
"id": pack.StickerPackID,
|
|
||||||
"name": pack.Name,
|
|
||||||
"description": pack.Description,
|
|
||||||
"publisher": pack.Publisher,
|
|
||||||
"animated": pack.Animated > 0,
|
|
||||||
"lottie": pack.Lottie > 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
content := &event.ImagePackEventContent{
|
|
||||||
Images: make(map[string]*event.ImagePackImage, len(pack.Stickers)),
|
|
||||||
Metadata: event.ImagePackMetadata{
|
|
||||||
DisplayName: pack.Name,
|
|
||||||
AvatarURL: "",
|
|
||||||
Usage: []event.ImagePackUsage{event.ImagePackUsageSticker},
|
|
||||||
Attribution: fmt.Sprintf("By %s on WhatsApp %s", pack.Publisher, canonicalURL),
|
|
||||||
BridgedPack: &event.BridgedStickerPack{
|
|
||||||
Network: StickerSourceID,
|
|
||||||
URL: canonicalURL,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ctx = context.WithValue(ctx, contextKeyClient, client)
|
|
||||||
ctx = context.WithValue(ctx, contextKeyIntent, mc.Bridge.Bot)
|
|
||||||
ctx = context.WithValue(ctx, contextKeyPortal, (*bridgev2.Portal)(nil))
|
|
||||||
for i, sticker := range pack.Stickers {
|
|
||||||
shortcode := sticker.PreviewWebpID
|
|
||||||
if shortcode == "" {
|
|
||||||
shortcode = fmt.Sprintf("%s_img%d", pack.StickerPackID, i+1)
|
|
||||||
}
|
|
||||||
body := sticker.AccessibilityText
|
|
||||||
var emoji string
|
|
||||||
if len(sticker.Emojis) > 0 {
|
|
||||||
emoji = sticker.Emojis[0]
|
|
||||||
if body == "" {
|
|
||||||
body = strings.Join(sticker.Emojis, " ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
part := &PreparedMedia{
|
|
||||||
Type: event.EventSticker,
|
|
||||||
MessageEventContent: &event.MessageEventContent{
|
|
||||||
Body: body,
|
|
||||||
Info: &event.FileInfo{
|
|
||||||
MimeType: sticker.MimeType,
|
|
||||||
Width: sticker.Width,
|
|
||||||
Height: sticker.Height,
|
|
||||||
Size: int(sticker.FileSize),
|
|
||||||
BridgedSticker: &event.BridgedSticker{
|
|
||||||
Network: StickerSourceID,
|
|
||||||
ID: base64.StdEncoding.EncodeToString(sticker.FileHash),
|
|
||||||
Emoji: emoji,
|
|
||||||
PackURL: canonicalURL,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
TypeDescription: "sticker",
|
|
||||||
}
|
|
||||||
dbKey := database.Key(fmt.Sprintf("stickercache:%x", part.Info.BridgedSticker.ID))
|
|
||||||
fixStickerDimensions(part.Info)
|
|
||||||
var packed *event.ImagePackImage
|
|
||||||
if mc.DirectMedia {
|
|
||||||
dbKey = ""
|
|
||||||
if part.Info.MimeType == "application/was" {
|
|
||||||
part.Info.MimeType = "video/lottie+json"
|
|
||||||
}
|
|
||||||
part.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeStickerPackMediaID(pack.StickerPackID, sticker.FileHash, userLoginID))
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("failed to generate content URI: %w", err))
|
|
||||||
}
|
|
||||||
} else if cached := mc.Bridge.DB.KV.Get(ctx, dbKey); cached != "" {
|
|
||||||
err = json.Unmarshal([]byte(cached), &packed)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal cached sticker data: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = mc.reuploadWhatsAppAttachment(ctx, sticker, part)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to reupload sticker %q: %w", sticker.GetDirectPath(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if packed == nil {
|
|
||||||
packed = &event.ImagePackImage{
|
|
||||||
URL: part.URL,
|
|
||||||
Body: part.Body,
|
|
||||||
Info: part.Info,
|
|
||||||
}
|
|
||||||
if dbKey != "" {
|
|
||||||
data, _ := json.Marshal(packed)
|
|
||||||
if data != nil {
|
|
||||||
mc.Bridge.DB.KV.Set(ctx, dbKey, string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
content.Images[shortcode] = packed
|
|
||||||
}
|
|
||||||
|
|
||||||
return &bridgev2.ImportedImagePack{
|
|
||||||
Content: content,
|
|
||||||
Extra: topLevelExtra,
|
|
||||||
Shortcode: pack.StickerPackID,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type StickerMetadata struct {
|
|
||||||
StickerPackID string `json:"sticker-pack-id"`
|
|
||||||
AccessibilityText string `json:"accessibility-text"`
|
|
||||||
Emojis []string `json:"emojis"`
|
|
||||||
IsFirstPartySticker int `json:"is-first-party-sticker"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sm *StickerMetadata) ToMatrix(content *event.MessageEventContent) {
|
|
||||||
if sm == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if sm.StickerPackID != "" && content.Info.BridgedSticker == nil {
|
|
||||||
content.Info.BridgedSticker = &event.BridgedSticker{
|
|
||||||
Network: StickerSourceID,
|
|
||||||
PackURL: StickerPackURLPrefix + sm.StickerPackID,
|
|
||||||
}
|
|
||||||
if len(sm.Emojis) > 0 {
|
|
||||||
content.Info.BridgedSticker.Emoji = sm.Emojis[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sm.AccessibilityText != "" {
|
|
||||||
content.Body = sm.AccessibilityText
|
|
||||||
} else if len(sm.Emojis) > 0 {
|
|
||||||
content.Body = strings.Join(sm.Emojis, " ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const StickerSourceID = "whatsapp"
|
|
||||||
const StickerPackURLPrefix = "https://wa.me/stickerpack/"
|
|
||||||
|
|
||||||
func PackAnimatedSticker(data []byte) ([]byte, error) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
zipWriter := zip.NewWriter(&buf)
|
|
||||||
f, err := zipWriter.Create("animation/animation.json")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create zip entry: %w", err)
|
|
||||||
}
|
|
||||||
_, err = f.Write(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write zip entry: %w", err)
|
|
||||||
}
|
|
||||||
err = zipWriter.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to close zip writer: %w", err)
|
|
||||||
}
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExtractAnimatedSticker(data []byte) ([]byte, *StickerMetadata, error) {
|
|
||||||
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to read sticker zip: %w", err)
|
|
||||||
}
|
|
||||||
animationFile, err := zipReader.Open("animation/animation.json")
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to open animation.json: %w", err)
|
|
||||||
}
|
|
||||||
animationFileInfo, err := animationFile.Stat()
|
|
||||||
if err != nil {
|
|
||||||
_ = animationFile.Close()
|
|
||||||
return nil, nil, fmt.Errorf("failed to stat animation.json: %w", err)
|
|
||||||
} else if animationFileInfo.Size() > uploadFileThreshold {
|
|
||||||
_ = animationFile.Close()
|
|
||||||
return nil, nil, fmt.Errorf("animation.json is too large (%.2f MiB)", float64(animationFileInfo.Size())/1024/1024)
|
|
||||||
}
|
|
||||||
data, err = io.ReadAll(animationFile)
|
|
||||||
_ = animationFile.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to read animation.json: %w", err)
|
|
||||||
}
|
|
||||||
var meta StickerMetadata
|
|
||||||
metaFile, err := zipReader.Open("animation/animation.json.overridden_metadata")
|
|
||||||
if err == nil {
|
|
||||||
_ = json.NewDecoder(metaFile).Decode(&meta)
|
|
||||||
_ = metaFile.Close()
|
|
||||||
}
|
|
||||||
if meta.StickerPackID == "" {
|
|
||||||
res := gjson.GetBytes(data, "metadata.customProps")
|
|
||||||
if res.IsObject() {
|
|
||||||
_ = json.Unmarshal(exstrings.UnsafeBytes(res.Raw), &meta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data, &meta, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MessageConverter) extractAnimatedSticker(fileInfo *PreparedMedia, data []byte) ([]byte, error) {
|
|
||||||
data, meta, err := ExtractAnimatedSticker(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
meta.ToMatrix(fileInfo.MessageEventContent)
|
|
||||||
fileInfo.Info.MimeType = "video/lottie+json"
|
|
||||||
fileInfo.FileName = "sticker.json"
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MessageConverter) convertAnimatedSticker(ctx context.Context, fileInfo *PreparedMedia, data []byte) ([]byte, []byte, *event.FileInfo, error) {
|
|
||||||
data, err := mc.extractAnimatedSticker(fileInfo, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
c := mc.AnimatedStickerConfig
|
|
||||||
if c.Target == "disable" {
|
|
||||||
return data, nil, nil, nil
|
|
||||||
} else if !lottie.Supported() {
|
|
||||||
zerolog.Ctx(ctx).Warn().Msg("Animated sticker conversion is enabled, but lottieconverter is not installed")
|
|
||||||
return data, nil, nil, nil
|
|
||||||
}
|
|
||||||
input := bytes.NewReader(data)
|
|
||||||
fileInfo.Info.MimeType = "image/" + c.Target
|
|
||||||
fileInfo.FileName = "sticker." + c.Target
|
|
||||||
switch c.Target {
|
|
||||||
case "png":
|
|
||||||
var output bytes.Buffer
|
|
||||||
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, "1")
|
|
||||||
return output.Bytes(), nil, nil, err
|
|
||||||
case "gif":
|
|
||||||
var output bytes.Buffer
|
|
||||||
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS))
|
|
||||||
return output.Bytes(), nil, nil, err
|
|
||||||
case "webm", "webp":
|
|
||||||
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("mautrix-whatsapp-lottieconverter-%s.%s", random.String(10), c.Target))
|
|
||||||
defer func() {
|
|
||||||
_ = os.Remove(tmpFile)
|
|
||||||
}()
|
|
||||||
thumbnailData, err := lottie.FFmpegConvert(ctx, input, tmpFile, c.Args.Width, c.Args.Height, c.Args.FPS)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
data, err = os.ReadFile(tmpFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, fmt.Errorf("failed to read converted file: %w", err)
|
|
||||||
}
|
|
||||||
var thumbnailInfo *event.FileInfo
|
|
||||||
if thumbnailData != nil {
|
|
||||||
thumbnailInfo = &event.FileInfo{
|
|
||||||
MimeType: "image/png",
|
|
||||||
Width: c.Args.Width,
|
|
||||||
Height: c.Args.Height,
|
|
||||||
Size: len(thumbnailData),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data, thumbnailData, thumbnailInfo, nil
|
|
||||||
default:
|
|
||||||
return nil, nil, nil, fmt.Errorf("unsupported target format %s", c.Target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MessageConverter) fillWebPStickerInfo(ctx context.Context, fileInfo *PreparedMedia, data []byte) {
|
|
||||||
meta, err := extractWebPStickerMetadata(data)
|
|
||||||
if err != nil {
|
|
||||||
zerolog.Ctx(ctx).Debug().Err(err).Msg("Failed to extract webp sticker metadata")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
meta.ToMatrix(fileInfo.MessageEventContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stickerMetadataEXIFTag is the custom EXIF tag WhatsApp uses to embed
|
|
||||||
// sticker pack metadata as a JSON object inside non-animated webp stickers.
|
|
||||||
const stickerMetadataEXIFTag = 0x5741
|
|
||||||
|
|
||||||
// extractWebPStickerMetadata parses the WhatsApp sticker pack metadata JSON
|
|
||||||
// embedded in EXIF tag 0x5741 of a non-animated webp sticker.
|
|
||||||
func extractWebPStickerMetadata(data []byte) (*StickerMetadata, error) {
|
|
||||||
exif, err := findWebPChunk(data, "EXIF")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
raw, err := findEXIFTagValue(exif, stickerMetadataEXIFTag)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var meta StickerMetadata
|
|
||||||
err = json.Unmarshal(raw, &meta)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse sticker metadata JSON: %w", err)
|
|
||||||
}
|
|
||||||
return &meta, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findWebPChunk(data []byte, chunkType string) ([]byte, error) {
|
|
||||||
if len(data) < 12 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WEBP" {
|
|
||||||
return nil, fmt.Errorf("not a webp file")
|
|
||||||
}
|
|
||||||
for pos := 12; pos+8 <= len(data); {
|
|
||||||
size := binary.LittleEndian.Uint32(data[pos+4 : pos+8])
|
|
||||||
start := pos + 8
|
|
||||||
end := start + int(size)
|
|
||||||
if end > len(data) {
|
|
||||||
return nil, fmt.Errorf("webp chunk %q extends past end of file", data[pos:pos+4])
|
|
||||||
}
|
|
||||||
if string(data[pos:pos+4]) == chunkType {
|
|
||||||
return data[start:end], nil
|
|
||||||
}
|
|
||||||
pos = end
|
|
||||||
if pos%2 != 0 {
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("webp chunk %q not found", chunkType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func findEXIFTagValue(exif []byte, tag uint16) ([]byte, error) {
|
|
||||||
if len(exif) < 8 {
|
|
||||||
return nil, fmt.Errorf("exif data too short")
|
|
||||||
}
|
|
||||||
var bo binary.ByteOrder
|
|
||||||
switch string(exif[0:2]) {
|
|
||||||
case "II":
|
|
||||||
bo = binary.LittleEndian
|
|
||||||
case "MM":
|
|
||||||
bo = binary.BigEndian
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid TIFF byte order %q", exif[0:2])
|
|
||||||
}
|
|
||||||
if bo.Uint16(exif[2:4]) != 0x002A {
|
|
||||||
return nil, fmt.Errorf("invalid TIFF magic")
|
|
||||||
}
|
|
||||||
ifdOffset := int(bo.Uint32(exif[4:8]))
|
|
||||||
if ifdOffset < 0 || ifdOffset+2 > len(exif) {
|
|
||||||
return nil, fmt.Errorf("IFD offset out of range")
|
|
||||||
}
|
|
||||||
count := int(bo.Uint16(exif[ifdOffset : ifdOffset+2]))
|
|
||||||
entries := ifdOffset + 2
|
|
||||||
if entries+count*12 > len(exif) {
|
|
||||||
return nil, fmt.Errorf("IFD entries out of range")
|
|
||||||
}
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
entry := exif[entries+i*12 : entries+(i+1)*12]
|
|
||||||
if bo.Uint16(entry[0:2]) != tag {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Tag 0x5741 stores JSON as type 7 (UNDEFINED), where size == count bytes.
|
|
||||||
size := int(bo.Uint32(entry[4:8]))
|
|
||||||
if size <= 4 {
|
|
||||||
return entry[8 : 8+size], nil
|
|
||||||
}
|
|
||||||
offset := int(bo.Uint32(entry[8:12]))
|
|
||||||
if offset+size > len(exif) {
|
|
||||||
return nil, fmt.Errorf("exif tag value out of range")
|
|
||||||
}
|
|
||||||
return exif[offset : offset+size], nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("exif tag 0x%04x not found", tag)
|
|
||||||
}
|
|
||||||
|
|
@ -33,7 +33,6 @@ const (
|
||||||
mediaIDTypeMessage = 255
|
mediaIDTypeMessage = 255
|
||||||
mediaIDTypeAvatar = 254
|
mediaIDTypeAvatar = 254
|
||||||
mediaIDTypeCommunityAvatar = 253
|
mediaIDTypeCommunityAvatar = 253
|
||||||
mediaIDTypeStickerPackItem = 252
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID) networkid.MediaID {
|
func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID) networkid.MediaID {
|
||||||
|
|
@ -83,28 +82,9 @@ type AvatarMediaInfo struct {
|
||||||
Community bool
|
Community bool
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
|
||||||
mediaID = append(mediaID, mediaIDTypeStickerPackItem)
|
|
||||||
mediaID = append(mediaID, byte(len(packID)))
|
|
||||||
mediaID = append(mediaID, packID...)
|
|
||||||
mediaID = append(mediaID, byte(len(fileHash)))
|
|
||||||
mediaID = append(mediaID, fileHash...)
|
|
||||||
mediaID = append(mediaID, byte(len(receiverID)))
|
|
||||||
mediaID = append(mediaID, receiverID...)
|
|
||||||
return mediaID
|
|
||||||
}
|
|
||||||
|
|
||||||
type StickerPackMediaInfo struct {
|
|
||||||
PackID string
|
|
||||||
FileHash []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParsedMediaID struct {
|
type ParsedMediaID struct {
|
||||||
Message *ParsedMessageID
|
Message *ParsedMessageID
|
||||||
Avatar *AvatarMediaInfo
|
Avatar *AvatarMediaInfo
|
||||||
Sticker *StickerPackMediaInfo
|
|
||||||
UserLogin networkid.UserLoginID
|
UserLogin networkid.UserLoginID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,24 +138,6 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) {
|
||||||
Community: mediaIDType == mediaIDTypeCommunityAvatar,
|
Community: mediaIDType == mediaIDTypeCommunityAvatar,
|
||||||
}
|
}
|
||||||
parsed.UserLogin = MakeUserLoginID(receiverID)
|
parsed.UserLogin = MakeUserLoginID(receiverID)
|
||||||
case mediaIDTypeStickerPackItem:
|
|
||||||
packID, err := readCompact(&mediaID, parseString)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse sticker pack ID: %w", err)
|
|
||||||
}
|
|
||||||
fileHash, err := readCompact(&mediaID, rawBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse sticker file hash: %w", err)
|
|
||||||
}
|
|
||||||
receiverID, err := readCompact(&mediaID, parseCompactJID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse receiver JID: %w", err)
|
|
||||||
}
|
|
||||||
parsed.Sticker = &StickerPackMediaInfo{
|
|
||||||
PackID: packID,
|
|
||||||
FileHash: fileHash,
|
|
||||||
}
|
|
||||||
parsed.UserLogin = MakeUserLoginID(receiverID)
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown media ID type %d", mediaIDType)
|
return nil, fmt.Errorf("unknown media ID type %d", mediaIDType)
|
||||||
}
|
}
|
||||||
|
|
@ -284,10 +246,6 @@ func parseCompactJID(jid []byte) (types.JID, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rawBytes(data []byte) ([]byte, error) {
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readCompact[T any](data *networkid.MediaID, fn func(data []byte) (T, error)) (T, error) {
|
func readCompact[T any](data *networkid.MediaID, fn func(data []byte) (T, error)) (T, error) {
|
||||||
var defVal T
|
var defVal T
|
||||||
if len(*data) < 1 {
|
if len(*data) < 1 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue