diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 4b3b934..3703df9 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -7,12 +7,10 @@ type: Bug --- - + - -* [ ] 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: `` +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 +ignored or closed immediately. +--> diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 343443b..ae5f5aa 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,14 +11,14 @@ jobs: strategy: fail-fast: false matrix: - go-version: ["1.25", "1.26"] - name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }} + go-version: ["1.24", "1.25"] + name: Lint ${{ matrix.go-version == '1.25' && '(latest)' || '(old)' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v6 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} cache: true diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index cae8a4d..0ae50e4 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: lock-stale: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v6 + - uses: dessant/lock-threads@v5 id: lock with: issue-inactive-days: 90 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bfb536..a48f8b8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: check-added-large-files - repo: https://github.com/tekwizely/pre-commit-golang - rev: v1.0.0-rc.4 + rev: v1.0.0-rc.2 hooks: - id: go-imports-repo args: diff --git a/CHANGELOG.md b/CHANGELOG.md index 892be2e..7e65fef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,62 +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 - -* Added option to save outgoing messages in the database to allow encryption - retries to work across restarts. -* Fixed contact list API not returning some contacts. -* Fixed business template messages with media duplicating the text part. - -# v26.02 - -* Bumped minimum Go version to 1.25. -* Added automatic recovery for WhatsApp app state sync issues. -* Fixed LID redirects for some non-message events. - -# v26.01 - -* Fixed broadcast list messages to LIDs causing split DMs. -* Fixed read receipts not working correctly in LID DMs. -* Fixed backfill sometimes racing with receiving LID mappings. - -# v25.12 - -* Updated Docker image to Alpine 3.23. -* Fixed group member invites from Matrix not automatically disinviting the phone - number ghost when the invite is redirected to a LID ghost. - -# v25.11 - -* Added interface support for notifying about failed invites when creating a - group and sending the invites via DM (only applicable to provisioning API). -* Added migration to automatically delete duplicate LID DM portals that were - created earlier. -* Changed contact list API to only include actual phone contacts. -* Removed extra unrecognized message notice when receiving live photos - (bridging the live photo video is not currently planned). -* Fixed pairing not working with latest WhatsApp Android version. -* Fixed replies, read receipts and typing notifications not being bridged - correctly after DM LID migration. -* Fixed backfill creating duplicate portals if history sync contains both LID - and phone number DM data. -* Fixed some cases of LID and phone number user infos getting out of sync. -* Fixed muting chat forever not being bridged correctly from WhatsApp. -* Fixed old mutes being re-applied on chat resync in some cases. -* Fixed backfilling failing if some reactions were missing sender info. -* Fixed space not being deleted when leaving community on WhatsApp. -* Fixed sticker size metadata on Matrix not matching how native WhatsApp Web - renders them. -* Fixed ratelimit errors in login not being exposed to the user properly - (thanks to [@dead8309] in [#852]). - -[@dead8309]: https://github.com/dead8309 -[#852]: https://github.com/mautrix/whatsapp/pull/852 - # v25.10 * Switched to calendar versioning. diff --git a/Dockerfile b/Dockerfile index 4efc9d5..3b71e4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1-alpine3.23 AS builder +FROM golang:1-alpine3.22 AS builder RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev @@ -6,7 +6,7 @@ COPY . /build WORKDIR /build RUN ./build.sh -FROM alpine:3.23 +FROM alpine:3.22 ENV UID=1337 \ GID=1337 diff --git a/Dockerfile.ci b/Dockerfile.ci index cb3be51..16ac07d 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1,6 +1,6 @@ ARG DOCKER_HUB="docker.io" -FROM ${DOCKER_HUB}/alpine:3.23 +FROM ${DOCKER_HUB}/alpine:3.22 ENV UID=1337 \ GID=1337 diff --git a/build.sh b/build.sh index 2442135..0676fdd 100755 --- a/build.sh +++ b/build.sh @@ -1,2 +1,4 @@ #!/bin/sh -BINARY_NAME=mautrix-whatsapp go tool maubuild "$@" +MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }') +GO_LDFLAGS="-s -w -X main.Tag=$(git describe --exact-match --tags 2>/dev/null) -X main.Commit=$(git rev-parse HEAD) -X 'main.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'" +go build -ldflags="$GO_LDFLAGS" "$@" ./cmd/mautrix-whatsapp diff --git a/cmd/mautrix-whatsapp/legacyprovision.go b/cmd/mautrix-whatsapp/legacyprovision.go index f0527b2..ef4d984 100644 --- a/cmd/mautrix-whatsapp/legacyprovision.go +++ b/cmd/mautrix-whatsapp/legacyprovision.go @@ -6,10 +6,7 @@ import ( "github.com/rs/zerolog/hlog" "go.mau.fi/util/exhttp" - "go.mau.fi/whatsmeow" - "go.mau.fi/whatsmeow/appstate" "go.mau.fi/whatsmeow/types" - "maunium.net/go/mautrix" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/matrix" "maunium.net/go/mautrix/id" @@ -113,41 +110,3 @@ func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) { }, }) } - -func provAppStateDebug(w http.ResponseWriter, r *http.Request) { - userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r) - if userLogin == nil { - return - } - client := userLogin.Client.(*connector.WhatsAppClient) - if client.Client == nil { - mautrix.MNotFound.WithMessage("WhatsApp client not connected").Write(w) - return - } - client.Client.AppStateDebugLogs = true - err := client.Client.FetchAppState(r.Context(), appstate.WAPatchName(r.PathValue("patch")), r.URL.Query().Get("full") == "1", false) - client.Client.AppStateDebugLogs = false - if err != nil { - mautrix.MUnknown.WithMessage("Failed to fetch app state: %v", err).Write(w) - } else { - exhttp.WriteEmptyJSONResponse(w, http.StatusOK) - } -} - -func provRecoverAppStateDebug(w http.ResponseWriter, r *http.Request) { - userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r) - if userLogin == nil { - return - } - client := userLogin.Client.(*connector.WhatsAppClient) - if client.Client == nil { - mautrix.MNotFound.WithMessage("WhatsApp client not connected").Write(w) - return - } - resp, err := client.Client.SendPeerMessage(r.Context(), whatsmeow.BuildAppStateRecoveryRequest(appstate.WAPatchName(r.PathValue("patch")))) - if err != nil { - mautrix.MUnknown.WithMessage("Failed to send app state recovery request: %v", err).Write(w) - } else { - exhttp.WriteJSONResponse(w, http.StatusOK, resp) - } -} diff --git a/cmd/mautrix-whatsapp/main.go b/cmd/mautrix-whatsapp/main.go index b97bc7f..a3e0665 100644 --- a/cmd/mautrix-whatsapp/main.go +++ b/cmd/mautrix-whatsapp/main.go @@ -18,7 +18,7 @@ var m = mxmain.BridgeMain{ Name: "mautrix-whatsapp", URL: "https://github.com/mautrix/whatsapp", Description: "A Matrix-WhatsApp puppeting bridge.", - Version: "26.04", + Version: "25.10", SemCalVer: true, Connector: &connector.WhatsAppConnector{}, } @@ -29,8 +29,6 @@ func main() { m.Matrix.Provisioning.Router.HandleFunc("GET /v1/contacts", legacyProvContacts) m.Matrix.Provisioning.Router.HandleFunc("GET /v1/resolve_identifier/{number}", legacyProvResolveIdentifier) m.Matrix.Provisioning.Router.HandleFunc("POST /v1/pm/{number}", legacyProvResolveIdentifier) - m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/appstate/{patch}", provAppStateDebug) - m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/recover-appstate/{patch}", provRecoverAppStateDebug) } } m.InitVersion(Tag, Commit, BuildTime) diff --git a/cmd/mautrix-whatsapp/plugin.go b/cmd/mautrix-whatsapp/plugin.go deleted file mode 100644 index a1c9ca0..0000000 --- a/cmd/mautrix-whatsapp/plugin.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build amd64 && cgo && !noplugin - -package main - -import ( - "fmt" - "os" - "plugin" - - "go.mau.fi/util/exerrors" - - "go.mau.fi/mautrix-whatsapp/pkg/connector" -) - -func init() { - path := os.Getenv("WM_PLUGIN_PATH") - if path == "" { - return - } - fmt.Println("Loading plugin from", path) - plug := exerrors.Must(plugin.Open(path)) - sym := exerrors.Must(plug.Lookup("NewClient")) - connector.NewMC = sym.(connector.NewMCFunc) -} diff --git a/go.mod b/go.mod index 9cd6c8f..b60775a 100644 --- a/go.mod +++ b/go.mod @@ -1,53 +1,51 @@ module go.mau.fi/mautrix-whatsapp -go 1.25.0 +go 1.24.0 -toolchain go1.26.2 - -tool go.mau.fi/util/cmd/maubuild +toolchain go1.25.3 require ( - github.com/lib/pq v1.12.3 - github.com/rs/zerolog v1.35.1 - github.com/tidwall/gjson v1.18.0 - go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25 + github.com/lib/pq v1.10.9 + github.com/rs/zerolog v1.34.0 + go.mau.fi/util v0.9.2 go.mau.fi/webp v0.2.0 - go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f - golang.org/x/image v0.39.0 - golang.org/x/net v0.53.0 - golang.org/x/sync v0.20.0 - google.golang.org/protobuf v1.36.11 + go.mau.fi/whatsmeow v0.0.0-20251023183934-2ced35dd7e8c + golang.org/x/image v0.32.0 + golang.org/x/net v0.46.0 + golang.org/x/sync v0.17.0 + google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4 + maunium.net/go/mautrix v0.25.3-0.20251024094209-5d87d14b8858 ) require ( - filippo.io/edwards25519 v1.2.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect github.com/beeper/argo-go v1.1.2 // 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.5.0 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.44 // indirect - github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect + github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rs/xid v1.6.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect - github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect - github.com/yuin/goldmark v1.8.2 // indirect + github.com/yuin/goldmark v1.7.13 // indirect go.mau.fi/libsignal v0.2.1 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect - golang.org/x/crypto v0.50.0 // indirect - golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect maunium.net/go/mauflag v1.0.0 // indirect diff --git a/go.sum b/go.sum index be22fdf..a67ba28 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= -filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= @@ -10,17 +10,20 @@ 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/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= 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.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= +github.com/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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -28,17 +31,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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +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/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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= -github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= -github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM= -github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4= +github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= @@ -46,8 +53,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/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= 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.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +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/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= @@ -57,9 +64,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= -github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -67,37 +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/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= -github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= -github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 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/util v0.9.9-0.20260511124621-9241e81bdf25 h1:YPEmc+li7TF6C9AdRTcSLMb6yCHdF27/wNT7kFLIVNg= -go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs= +go.mau.fi/util v0.9.2 h1:+S4Z03iCsGqU2WY8X2gySFsFjaLlUHFRDVCYvVwynKM= +go.mau.fi/util v0.9.2/go.mod h1:055elBBCJSdhRsmub7ci9hXZPgGr1U6dYg44cSgRgoU= go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg= 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-20260513140310-c551a4055c0f/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg= +go.mau.fi/whatsmeow v0.0.0-20251023183934-2ced35dd7e8c h1:7QLq7oW+q8U5AU7uhGsVOB2qPD3306fr7oVSni77c+A= +go.mau.fi/whatsmeow v0.0.0-20251023183934-2ced35dd7e8c/go.mod h1:VJq+D05Fe5EroZxs2StEYD/AsWJO2aQ7Niucz7lCvao= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= -golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= -golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -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/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA= +golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= +golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -107,5 +113,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4 h1:zNC9eVAhw8FhKpM3AxNAh/iy75UEYX91uJUvqqAYlvo= -maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4/go.mod h1:3sOGhXi3P1V6/NruTA0gujkvTypXVUraWktCuTGyDuM= +maunium.net/go/mautrix v0.25.3-0.20251024094209-5d87d14b8858 h1:w4rgGYjb+lA/AIbyOaUAMDQ6KhxyrTxNxZMPljaXoYw= +maunium.net/go/mautrix v0.25.3-0.20251024094209-5d87d14b8858/go.mod h1:EWgYyp2iFZP7pnSm+rufHlO8YVnA2KnoNBDpwekiAwI= diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index b4ef181..2c3a14e 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -63,6 +63,11 @@ func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) { for { var resetTimer bool select { + case evt := <-wa.historySyncs: + // The timer is stopped unconditionally and restarted if either handleWAHistorySync had conversations, + // or if the timer was previously started and hadn't reached the loop above yet. + dispatchTimer.Stop() + resetTimer, _ = wa.handleWAHistorySync(ctx, evt, false) case <-wa.historySyncWakeup: dispatchTimer.Stop() notif, rowid, err := wa.Main.DB.HSNotif.GetNext(ctx, wa.UserLogin.ID) @@ -115,30 +120,14 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, Uint32("chunk_order", evt.GetChunkOrder()). Uint32("progress", evt.GetProgress()). Logger() - log.Debug(). - 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") + log.Debug().Msg("Downloading history sync") blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true) if err != nil { log.Err(err).Msg("Failed to download history sync") 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) { - innerErr = wa.handleWAHistorySync(ctx, evt, blob, true) + resetTimer, innerErr = wa.handleWAHistorySync(ctx, blob, true) if innerErr != nil { return } @@ -150,28 +139,13 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, }) if err != nil { 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 } -func (wa *WhatsAppClient) handleWAHistorySync( - ctx context.Context, - notif *waE2E.HistorySyncNotification, - evt *waHistorySync.HistorySync, - stopOnError bool, -) error { +func (wa *WhatsAppClient) handleWAHistorySync(ctx context.Context, evt *waHistorySync.HistorySync, stopOnError bool) (bool, error) { if evt == nil || evt.SyncType == nil { - return nil + return false, nil } log := wa.UserLogin.Log.With(). Str("action", "store history sync"). @@ -196,57 +170,36 @@ func (wa *WhatsAppClient) handleWAHistorySync( Int("recent_sticker_count", len(evt.GetRecentStickers())). Int("past_participant_count", len(evt.GetPastParticipants())). Msg("Ignoring history sync") - return nil + return false, nil } log.Info(). Int("conversation_count", len(evt.GetConversations())). Int("past_participant_count", len(evt.GetPastParticipants())). - Dict("notification_metadata", zerolog.Dict(). - Int64("oldest_msg_in_chunk_ts", notif.GetOldestMsgInChunkTimestampSec()). - Any("full_request_meta", notif.GetFullHistorySyncOnDemandRequestMetadata()). - Any("access_status", notif.GetMessageAccessStatus()). - Str("peer_data_request_session_id", notif.GetPeerDataRequestSessionID())). Msg("Storing history sync") start := time.Now() successfullySavedTotal := 0 failedToSaveTotal := 0 totalMessageCount := 0 for _, conv := range evt.GetConversations() { - log := log.With(). - Int("msg_count", len(conv.GetMessages())). - Logger() jid, err := types.ParseJID(conv.GetID()) if err != nil { totalMessageCount += len(conv.GetMessages()) log.Warn().Err(err). Str("chat_jid", conv.GetID()). + Int("msg_count", len(conv.GetMessages())). Msg("Failed to parse chat JID in history sync") continue } else if jid.Server == types.BroadcastServer { log.Debug().Stringer("chat_jid", jid).Msg("Skipping broadcast list in history sync") continue - } else { - totalMessageCount += len(conv.GetMessages()) } - if jid.Server == types.HiddenUserServer { - pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid) - if err != nil { - log.Err(err).Stringer("lid", jid).Msg("Failed to get PN for LID in history sync") - } else if pn.IsEmpty() { - log.Warn().Stringer("lid", jid).Msg("No PN found for LID in history sync") - } else { - log.Debug(). - Stringer("lid", jid). - Stringer("pn", pn). - Msg("Rerouting LID DM to phone number in history sync") - jid = pn - } - } - log.UpdateContext(func(c zerolog.Context) zerolog.Context { - return c.Stringer("chat_jid", jid) - }) + totalMessageCount += len(conv.GetMessages()) + log := log.With(). + Stringer("chat_jid", jid). + Int("msg_count", len(conv.GetMessages())). + Logger() - var minTime, maxTime, firstItemTime, lastItemTime time.Time + var minTime, maxTime time.Time var minTimeIndex, maxTimeIndex int ignoredTypes := 0 @@ -262,10 +215,6 @@ func (wa *WhatsAppClient) handleWAHistorySync( Msg("Dropping historical message due to parse error") continue } - if firstItemTime.IsZero() { - firstItemTime = msgEvt.Info.Timestamp - } - lastItemTime = msgEvt.Info.Timestamp if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) { minTime = msgEvt.Info.Timestamp minTimeIndex = i @@ -298,9 +247,6 @@ func (wa *WhatsAppClient) handleWAHistorySync( Int("lowest_time_index", minTimeIndex). Time("highest_time", maxTime). Int("highest_time_index", maxTimeIndex). - Time("first_item_time", firstItemTime). - Time("last_item_time", lastItemTime). - Bool("highest_time_mismatch", firstItemTime != maxTime). Dict("metadata", zerolog.Dict(). Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()). Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()). @@ -309,9 +255,7 @@ func (wa *WhatsAppClient) handleWAHistorySync( Bool("archived", conv.GetArchived()). Uint32("pinned", conv.GetPinned()). Uint64("mute_end", conv.GetMuteEndTime()). - Uint32("unread_count", conv.GetUnreadCount()). - Bool("end_of_history", conv.GetEndOfHistoryTransfer()). - Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()), + Uint32("unread_count", conv.GetUnreadCount()), ). Msg("Collected messages to save from history sync conversation") @@ -319,7 +263,7 @@ func (wa *WhatsAppClient) handleWAHistorySync( err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime)) if err != nil { if stopOnError { - return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err) + return false, fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err) } log.Err(err).Msg("Failed to save conversation metadata") continue @@ -327,7 +271,7 @@ func (wa *WhatsAppClient) handleWAHistorySync( err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages) if err != nil { 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") failedToSaveTotal += len(messages) @@ -337,7 +281,7 @@ func (wa *WhatsAppClient) handleWAHistorySync( err = wa.Main.Bridge.DB.BackfillTask.MarkNotDone(ctx, wa.makeWAPortalKey(jid), wa.UserLogin.ID) if err != nil { 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") } @@ -349,7 +293,9 @@ func (wa *WhatsAppClient) handleWAHistorySync( Int("total_message_count", totalMessageCount). Dur("duration", time.Since(start)). 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) { @@ -396,7 +342,7 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) { log.Warn().Err(ctx.Err()).Msg("Context cancelled, stopping history sync portal creation") return } - wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true) + wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv) if errors.Is(err, whatsmeow.ErrNotInGroup) { log.Debug().Stringer("chat_jid", conv.ChatJID). Msg("Skipping creating room because the user is not a participant") @@ -473,67 +419,38 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet } var markRead bool 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.AnchorMessage != nil { 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 } - } else { - if params.AnchorMessage != nil { - endTime = ptr.Ptr(params.AnchorMessage.Timestamp) + } else 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) } - 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 - } - } - } - var anchorID types.MessageID - if params.AnchorMessage != nil { - parsedID, _ := waid.ParseMessageID(params.AnchorMessage.ID) - if parsedID != nil { - 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 + endTime = ptr.Ptr(time.Unix(endTimeUnix, 0)) + } else if params.AnchorMessage != nil { + endTime = ptr.Ptr(params.AnchorMessage.Timestamp) } messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1) if err != nil { return nil, fmt.Errorf("failed to load messages from database: %w", err) - } else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) { - wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0) - if hasMore && !params.AllowSlowFetch { - return &bridgev2.FetchMessagesResponse{ - MoreRequiresSlowFetch: true, - HasMore: true, - Forward: params.Forward, - }, nil - } else if hasMore { - return wa.fetchMessagesFromPhone(ctx, params) - } + } else if len(messages) == 0 { return &bridgev2.FetchMessagesResponse{ HasMore: false, Forward: params.Forward, }, nil } + hasMore := false + oldestTS := messages[len(messages)-1].GetMessageTimestamp() + newestTS := messages[0].GetMessageTimestamp() if len(messages) > params.Count { - oldestTS := messages[len(messages)-1].GetMessageTimestamp() hasMore = true // 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. @@ -544,78 +461,19 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet } } } - resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true) - 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)) + convertedMessages := make([]*bridgev2.BackfillMessage, len(messages)) var mediaRequests []*wadb.MediaRequest for i, msg := range messages { evt, err := wa.Client.ParseWebMessage(portalJID, msg) if err != nil { - if explodeOnError { - // This should never happen because the info is already parsed once before being stored in the database - 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 - } + // 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) } + var mediaReq *wadb.MediaRequest isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension - converted, mediaReq := wa.convertHistorySyncMessage( - ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, + convertedMessages[i], mediaReq = wa.convertHistorySyncMessage( + ctx, params.Portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, ) - convertedMessages = append(convertedMessages, converted) if mediaReq != nil { mediaRequests = append(mediaRequests, mediaReq) } @@ -624,10 +482,24 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( return &bridgev2.FetchMessagesResponse{ Messages: convertedMessages, Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)), + HasMore: hasMore, + Forward: endTime == nil, + MarkRead: markRead, + // TODO set remaining or total count CompleteCallback: func() { // TODO this only deletes after backfilling. If there's no need for backfill after a relogin, // the messages will be stuck in the database - wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS) + 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 { go func(ctx context.Context) { for _, req := range mediaRequests { @@ -645,94 +517,6 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( }, 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( ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction, ) (*bridgev2.BackfillMessage, *wadb.MediaRequest) { @@ -750,10 +534,10 @@ func (wa *WhatsAppClient) convertHistorySyncMessage( TxnID: networkid.TransactionID(waid.MakeMessageID(info.Chat, info.Sender, info.ID)), Timestamp: info.Timestamp, StreamOrder: info.Timestamp.Unix(), - Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)), + Reactions: make([]*bridgev2.BackfillReaction, len(reactions)), } mediaReq := wa.processFailedMedia(ctx, portal.PortalKey, wrapped.ID, wrapped.ConvertedMessage, true) - for _, reaction := range reactions { + for i, reaction := range reactions { var sender types.JID if reaction.GetKey().GetFromMe() { sender = wa.JID @@ -765,12 +549,12 @@ func (wa *WhatsAppClient) convertHistorySyncMessage( if sender.IsEmpty() { continue } - wrapped.Reactions = append(wrapped.Reactions, &bridgev2.BackfillReaction{ + wrapped.Reactions[i] = &bridgev2.BackfillReaction{ TargetPart: ptr.Ptr(networkid.PartID("")), Timestamp: time.UnixMilli(reaction.GetSenderTimestampMS()), Sender: wa.makeEventSender(ctx, sender), Emoji: reaction.GetText(), - }) + } } return wrapped, mediaReq } diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index 3e6f658..ef7a273 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -8,7 +8,6 @@ import ( "go.mau.fi/util/jsontime" "go.mau.fi/util/ptr" "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/event" "go.mau.fi/mautrix-whatsapp/pkg/waid" @@ -19,7 +18,6 @@ var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{ AggressiveUpdateInfo: true, ImplicitReadReceipts: true, Provisioning: bridgev2.ProvisioningCapabilities{ - ImagePackImport: true, ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{ CreateDM: true, LookupPhone: true, @@ -52,7 +50,8 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit } func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) { - return 1, 8 + // Bump caps version to resend room features after adding room management capabilities + return 1, 6 } const WAMaxFileSize = 2000 * 1024 * 1024 @@ -67,7 +66,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel { } func capID() string { - base := "fi.mau.whatsapp.capabilities.2026_05_12" + base := "fi.mau.whatsapp.capabilities.2025_10_07" if ffmpeg.Supported() { return base + "+ffmpeg" } @@ -126,10 +125,10 @@ var whatsappCaps = &event.RoomFeatures{ event.CapMsgSticker: { MimeTypes: map[string]event.CapabilitySupportLevel{ "image/webp": event.CapLevelFullySupported, + // TODO see if sending lottie is possible + //"video/lottie+json": event.CapLevelFullySupported, "image/png": 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, MaxSize: WAMaxFileSize, @@ -163,22 +162,12 @@ var whatsappCaps = &event.RoomFeatures{ MaxSize: WAMaxFileSize, }, }, - State: event.StateFeatureMap{ - event.StateRoomName.Type: {Level: event.CapLevelFullySupported}, - event.StateRoomAvatar.Type: {Level: event.CapLevelFullySupported}, - event.StateTopic.Type: {Level: event.CapLevelFullySupported}, - event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported}, - }, - MemberActions: event.MemberFeatureMap{ - event.MemberActionInvite: event.CapLevelFullySupported, - event.MemberActionKick: event.CapLevelFullySupported, - event.MemberActionLeave: event.CapLevelFullySupported, - }, MaxTextLength: MaxTextLength, LocationMessage: event.CapLevelFullySupported, Poll: event.CapLevelFullySupported, Reply: event.CapLevelFullySupported, Edit: event.CapLevelFullySupported, + EditMaxCount: 10, EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)), Delete: event.CapLevelFullySupported, DeleteForMe: false, @@ -189,18 +178,16 @@ var whatsappCaps = &event.RoomFeatures{ TypingNotifications: true, DisappearingTimer: waDisappearingCap, DeleteChat: true, + + // Room management capabilities + ManageMembers: true, + SetRoomAvatar: true, + SetRoomTitle: true, } -var whatsappDMCaps *event.RoomFeatures var whatsappCAGCaps *event.RoomFeatures func init() { - whatsappDMCaps = ptr.Clone(whatsappCaps) - whatsappDMCaps.ID = capID() + "+dm" - whatsappDMCaps.State = event.StateFeatureMap{ - event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported}, - } - whatsappDMCaps.MemberActions = nil whatsappCAGCaps = ptr.Clone(whatsappCaps) whatsappCAGCaps.ID = capID() + "+cag" whatsappCAGCaps.Reply = event.CapLevelUnsupported @@ -210,8 +197,6 @@ func init() { func (wa *WhatsAppClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *event.RoomFeatures { if portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup { return whatsappCAGCaps - } else if portal.RoomType == database.RoomTypeDM { - return whatsappDMCaps } return whatsappCaps } diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 5555566..8d47564 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -26,10 +26,10 @@ func (wa *WhatsAppClient) GetChatInfo(ctx context.Context, portal *bridgev2.Port if err != nil { return nil, err } - return wa.getChatInfo(ctx, portalJID, nil, portal.MXID == "") + return wa.getChatInfo(ctx, portalJID, nil) } -func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation, isNew bool) (wrapped *bridgev2.ChatInfo, err error) { +func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation) (wrapped *bridgev2.ChatInfo, err error) { switch portalJID.Server { case types.DefaultUserServer, types.HiddenUserServer, types.BotServer: wrapped = wa.wrapDMInfo(ctx, portalJID) @@ -40,14 +40,14 @@ func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, return nil, fmt.Errorf("broadcast list bridging is currently not supported") } case types.GroupServer: - info, err := wa.Client.GetGroupInfo(ctx, portalJID) + info, err := wa.Client.GetGroupInfo(portalJID) if err != nil { return nil, err } wrapped = wa.wrapGroupInfo(ctx, info) wrapped.ExtraUpdates = bridgev2.MergeExtraUpdaters(wrapped.ExtraUpdates, updatePortalLastSyncAt) case types.NewsletterServer: - info, err := wa.Client.GetNewsletterInfo(ctx, portalJID) + info, err := wa.Client.GetNewsletterInfo(portalJID) if err != nil { return nil, err } @@ -55,23 +55,21 @@ func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, default: return nil, fmt.Errorf("unsupported server %s", portalJID.Server) } - wa.addExtrasToWrapped(ctx, portalJID, wrapped, conv, isNew) + wa.addExtrasToWrapped(ctx, portalJID, wrapped, conv) return wrapped, nil } -func (wa *WhatsAppClient) addExtrasToWrapped(ctx context.Context, portalJID types.JID, wrapped *bridgev2.ChatInfo, conv *wadb.Conversation, isNew bool) { - if isNew { - if conv == nil { - var err error - conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID) - if err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get history sync conversation info") - } - } - if conv != nil { - wa.applyHistoryInfo(wrapped, conv) +func (wa *WhatsAppClient) addExtrasToWrapped(ctx context.Context, portalJID types.JID, wrapped *bridgev2.ChatInfo, conv *wadb.Conversation) { + if conv == nil { + var err error + conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID) + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get history sync conversation info") } } + if conv != nil { + wa.applyHistoryInfo(wrapped, conv) + } wa.applyChatSettings(ctx, portalJID, wrapped) } @@ -421,7 +419,7 @@ func (wa *WhatsAppClient) makePortalAvatarFetcher(avatarID string, sender types. existingID = "" } var wrappedAvatar *bridgev2.Avatar - avatar, err := wa.Client.GetProfilePictureInfo(ctx, jid, &whatsmeow.GetProfilePictureParams{ + avatar, err := wa.Client.GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{ ExistingID: existingID, IsCommunity: portal.RoomType == database.RoomTypeSpace, }) @@ -494,7 +492,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne } else if info.ThreadMeta.Preview.ID != "" { avatar.ID = networkid.AvatarID(info.ThreadMeta.Preview.ID) avatar.Get = func(ctx context.Context) ([]byte, error) { - meta, err := wa.Client.GetNewsletterInfo(ctx, info.ID) + meta, err := wa.Client.GetNewsletterInfo(info.ID) if err != nil { return nil, fmt.Errorf("failed to fetch full res avatar info: %w", err) } else if meta.ThreadMeta.Picture == nil { diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 3f19bf7..6196e16 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -27,8 +27,8 @@ import ( "github.com/rs/zerolog" "go.mau.fi/util/exsync" "go.mau.fi/whatsmeow" - "go.mau.fi/whatsmeow/appstate" waBinary "go.mau.fi/whatsmeow/binary" + "go.mau.fi/whatsmeow/proto/waHistorySync" "go.mau.fi/whatsmeow/proto/waWa6" "go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/types" @@ -38,7 +38,6 @@ import ( "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/status" - "maunium.net/go/mautrix/event" "go.mau.fi/mautrix-whatsapp/pkg/waid" ) @@ -47,15 +46,14 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. w := &WhatsAppClient{ Main: wa, UserLogin: login, - MC: noopMCInstance, - historySyncWakeup: make(chan struct{}, 1), - resyncQueue: make(map[types.JID]resyncQueueItem), - directMediaRetries: make(map[networkid.MessageID]*directMediaRetry), - mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle), - pushNamesSynced: exsync.NewEvent(), - createDedup: exsync.NewSet[types.MessageID](), - appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time), + historySyncs: make(chan *waHistorySync.HistorySync, 64), + historySyncWakeup: make(chan struct{}, 1), + resyncQueue: make(map[types.JID]resyncQueueItem), + directMediaRetries: make(map[networkid.MessageID]*directMediaRetry), + mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle), + pushNamesSynced: exsync.NewEvent(), + createDedup: exsync.NewSet[types.MessageID](), } login.Client = w @@ -76,8 +74,10 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. w.Client = whatsmeow.NewClient(w.Device, waLog.Zerolog(log)) w.Client.AddEventHandlerWithSuccessStatus(w.handleWAEvent) w.Client.SynchronousAck = true - w.Client.EnableDecryptedEventBuffer = bridgev2.PortalEventBuffer == 0 - w.Client.ManualHistorySyncDownload = true + if bridgev2.PortalEventBuffer == 0 { + w.Client.EnableDecryptedEventBuffer = true + w.Client.ManualHistorySyncDownload = true + } w.Client.SendReportingTokens = true w.Client.AutomaticMessageRerequestFromPhone = true w.Client.GetMessageForRetry = w.trackNotFoundRetry @@ -85,7 +85,6 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. w.Client.BackgroundEventCtx = w.UserLogin.Log.WithContext(wa.Bridge.BackgroundCtx) w.Client.SetForceActiveDeliveryReceipts(wa.Config.ForceActiveDeliveryReceipts) w.Client.InitialAutoReconnect = wa.Config.InitialAutoReconnect - w.Client.UseRetryMessageStore = wa.Config.UseWhatsAppRetryStore } else { w.UserLogin.Log.Warn().Stringer("jid", w.JID).Msg("No device found for user in whatsmeow store") } @@ -104,8 +103,8 @@ type WhatsAppClient struct { Client *whatsmeow.Client Device *store.Device JID types.JID - MC mClient + historySyncs chan *waHistorySync.HistorySync historySyncWakeup chan struct{} stopLoops atomic.Pointer[context.CancelFunc] resyncQueue map[types.JID]resyncQueueItem @@ -119,9 +118,6 @@ type WhatsAppClient struct { pushNamesSynced *exsync.Event lastPresence types.Presence createDedup *exsync.Set[types.MessageID] - - appStateRecoveryLock sync.Mutex - appStateFullSyncAttempted map[appstate.WAPatchName]time.Time } var ( @@ -129,7 +125,6 @@ var ( _ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil) - _ bridgev2.StickerImportingNetworkAPI = (*WhatsAppClient)(nil) ) var pushCfg = &bridgev2.PushConfig{ @@ -197,20 +192,13 @@ func (wa *WhatsAppClient) Connect(ctx context.Context) { wa.UserLogin.BridgeState.Send(state) return } - wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting}) wa.Main.firstClientConnectOnce.Do(wa.Main.onFirstClientConnect) if err := wa.Main.updateProxy(ctx, wa.Client, false); err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to update proxy") } - if ctx.Err() != nil { - return - } - wa.initMC() wa.startLoops() wa.Client.BackgroundEventCtx = wa.UserLogin.Log.WithContext(wa.Main.Bridge.BackgroundCtx) - zerolog.Ctx(ctx).Debug().Msg("Connecting to WhatsApp") - if err := wa.Client.ConnectContext(ctx); err != nil { - wa.callStopLoops() + if err := wa.Client.Connect(); err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to connect to WhatsApp") state := status.BridgeState{ StateEvent: status.StateUnknownError, @@ -266,11 +254,9 @@ func (wa *WhatsAppClient) ConnectBackground(ctx context.Context, params *bridgev return payload } defer func() { - if cli := wa.Client; cli != nil { - cli.GetClientPayload = nil - } + wa.Client.GetClientPayload = nil }() - err := wa.Client.ConnectContext(ctx) + err := wa.Client.Connect() if err != nil { return err } @@ -295,7 +281,7 @@ func (wa *WhatsAppClient) ConnectBackground(ctx context.Context, params *bridgev func (wa *WhatsAppClient) sendPNData(ctx context.Context, pn string) error { //lint:ignore SA1019 this is supposed to be dangerous - resp, err := wa.Client.DangerousInternals().SendIQ(ctx, whatsmeow.DangerousInfoQuery{ + resp, err := wa.Client.DangerousInternals().SendIQ(whatsmeow.DangerousInfoQuery{ Namespace: "urn:xmpp:whatsapp:push", Type: "get", To: types.ServerJID, @@ -303,6 +289,7 @@ func (wa *WhatsAppClient) sendPNData(ctx context.Context, pn string) error { Tag: "pn", Content: pn, }}, + Context: ctx, }) if err != nil { return fmt.Errorf("failed to send pn: %w", err) @@ -317,7 +304,7 @@ func (wa *WhatsAppClient) sendPNData(ctx context.Context, pn string) error { } zerolog.Ctx(ctx).Debug().Str("cat_data", string(catContentBytes)).Msg("Received cat response from sending pn data") //lint:ignore SA1019 this is supposed to be dangerous - err = wa.Client.DangerousInternals().SendNode(ctx, waBinary.Node{ + err = wa.Client.DangerousInternals().SendNode(waBinary.Node{ Tag: "ib", Content: []waBinary.Node{{ Tag: "cat", @@ -332,12 +319,11 @@ func (wa *WhatsAppClient) sendPNData(ctx context.Context, pn string) error { } func (wa *WhatsAppClient) startLoops() { - ctx, cancel := context.WithCancel(wa.Main.Bridge.BackgroundCtx) + ctx, cancel := context.WithCancel(context.Background()) oldStop := wa.stopLoops.Swap(&cancel) if oldStop != nil { (*oldStop)() } - ctx = wa.UserLogin.Log.WithContext(ctx) go wa.historySyncLoop(ctx) go wa.ghostResyncLoop(ctx) if mrc := wa.Main.Config.HistorySync.MediaRequests; mrc.AutoRequestMedia && mrc.RequestMethod == MediaRequestMethodLocalTime { @@ -355,14 +341,10 @@ func (wa *WhatsAppClient) GetStore() *store.Device { return store.NoopDevice } -func (wa *WhatsAppClient) callStopLoops() { +func (wa *WhatsAppClient) Disconnect() { if stopHistorySyncLoop := wa.stopLoops.Swap(nil); stopHistorySyncLoop != nil { (*stopHistorySyncLoop)() } -} - -func (wa *WhatsAppClient) Disconnect() { - wa.callStopLoops() if cli := wa.Client; cli != nil { cli.Disconnect() } @@ -427,7 +409,7 @@ func (wa *WhatsAppClient) HandleMatrixViewingChat(ctx context.Context, msg *brid } if wa.lastPresence != presence { - err := wa.updatePresence(ctx, presence) + err := wa.updatePresence(presence) if err != nil { zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set presence when viewing chat") } @@ -462,19 +444,10 @@ func (wa *WhatsAppClient) HandleMatrixViewingChat(ctx context.Context, msg *brid return nil } -func (wa *WhatsAppClient) updatePresence(ctx context.Context, presence types.Presence) error { - err := wa.Client.SendPresence(ctx, presence) +func (wa *WhatsAppClient) updatePresence(presence types.Presence) error { + err := wa.Client.SendPresence(presence) if err == nil { wa.lastPresence = presence } 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 -} diff --git a/pkg/connector/commands.go b/pkg/connector/commands.go index 037d24e..ec1b999 100644 --- a/pkg/connector/commands.go +++ b/pkg/connector/commands.go @@ -17,15 +17,12 @@ package connector import ( - "context" "errors" "fmt" "html" - "slices" "strings" "github.com/rs/zerolog" - "go.mau.fi/util/exslices" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/appstate" "go.mau.fi/whatsmeow/types" @@ -68,7 +65,7 @@ func fnAccept(ce *commands.Event) { ce.Reply("Login not found") } else if !login.Client.IsLoggedIn() { ce.Reply("Not logged in") - } else if err = login.Client.(*WhatsAppClient).Client.JoinGroupWithInvite(ce.Ctx, meta.JID, meta.Inviter, meta.Code, meta.Expiration); err != nil { + } else if err = login.Client.(*WhatsAppClient).Client.JoinGroupWithInvite(meta.JID, meta.Inviter, meta.Code, meta.Expiration); err != nil { ce.Log.Err(err).Msg("Failed to accept group invite") ce.Reply("Failed to accept group invite: %v", err) } else { @@ -123,6 +120,9 @@ func fnSync(ce *commands.Event) { return } for _, group := range groups { + wrapped := wa.wrapGroupInfo(ce.Ctx, group) + wrapped.ExtraUpdates = bridgev2.MergeExtraUpdaters(wrapped.ExtraUpdates, updatePortalLastSyncAt) + wa.addExtrasToWrapped(ce.Ctx, group.JID, wrapped, nil) login.QueueRemoteEvent(&simplevent.ChatResync{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventChatResync, @@ -130,12 +130,7 @@ func fnSync(ce *commands.Event) { LogContext: logContext, CreatePortal: true, }, - GetChatInfoFunc: func(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.ChatInfo, error) { - wrapped := wa.wrapGroupInfo(ce.Ctx, group) - wrapped.ExtraUpdates = bridgev2.MergeExtraUpdaters(wrapped.ExtraUpdates, updatePortalLastSyncAt) - wa.addExtrasToWrapped(ce.Ctx, group.JID, wrapped, nil, portal.MXID == "") - return wrapped, nil - }, + ChatInfo: wrapped, }) } ce.Reply("Queued syncs for %d groups", len(groups)) @@ -146,17 +141,7 @@ func fnSync(ce *commands.Event) { wa.resyncContacts(true, false) ce.React("✅") case "appstate": - names := appstate.AllPatchNames[:] - if len(ce.Args) > 1 { - names = exslices.CastFuncFilter(ce.Args[1:], func(name string) (appstate.WAPatchName, bool) { - if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) { - ce.Reply("Invalid app state name `%s`", name) - return "", false - } - return appstate.WAPatchName(name), true - }) - } - for _, name := range names { + for _, name := range appstate.AllPatchNames { err := wa.Client.FetchAppState(ce.Ctx, name, true, false) if errors.Is(err, appstate.ErrKeyNotFound) { ce.Reply("Key not found error syncing app state %s: %v\n\nKey requests are sent automatically, and the sync should happen in the background after your phone responds.", name, err) @@ -204,7 +189,7 @@ func fnInviteLink(ce *commands.Event) { ce.Reply("Can't get invite link to private chat") } else if portalJID.IsBroadcastList() { ce.Reply("Can't get invite link to broadcast list") - } else if link, err := wa.Client.GetGroupInviteLink(ce.Ctx, portalJID, reset); err != nil { + } else if link, err := wa.Client.GetGroupInviteLink(portalJID, reset); err != nil { ce.Reply("Failed to get invite link: %v", err) } else { ce.Reply(link) @@ -234,14 +219,14 @@ func fnResolveLink(ce *commands.Event) { } wa := login.Client.(*WhatsAppClient) if strings.HasPrefix(ce.Args[0], whatsmeow.InviteLinkPrefix) { - group, err := wa.Client.GetGroupInfoFromLink(ce.Ctx, ce.Args[0]) + group, err := wa.Client.GetGroupInfoFromLink(ce.Args[0]) if err != nil { ce.Reply("Failed to get group info: %v", err) return } ce.Reply("That invite link points at %s (`%s`)", group.Name, group.JID) } else if strings.HasPrefix(ce.Args[0], whatsmeow.BusinessMessageLinkPrefix) || strings.HasPrefix(ce.Args[0], whatsmeow.BusinessMessageLinkDirectPrefix) { - target, err := wa.Client.ResolveBusinessMessageLink(ce.Ctx, ce.Args[0]) + target, err := wa.Client.ResolveBusinessMessageLink(ce.Args[0]) if err != nil { ce.Reply("Failed to get business info: %v", err) return @@ -256,7 +241,7 @@ func fnResolveLink(ce *commands.Event) { } ce.Reply("That link points at %s (+%s).%s", target.PushName, target.JID.User, message) } else if strings.HasPrefix(ce.Args[0], whatsmeow.ContactQRLinkPrefix) || strings.HasPrefix(ce.Args[0], whatsmeow.ContactQRLinkDirectPrefix) { - target, err := wa.Client.ResolveContactQRLink(ce.Ctx, ce.Args[0]) + target, err := wa.Client.ResolveContactQRLink(ce.Args[0]) if err != nil { ce.Reply("Failed to get contact info: %v", err) return @@ -295,7 +280,7 @@ func fnJoin(ce *commands.Event) { wa := login.Client.(*WhatsAppClient) if strings.HasPrefix(ce.Args[0], whatsmeow.InviteLinkPrefix) { - jid, err := wa.Client.JoinGroupWithLink(ce.Ctx, ce.Args[0]) + jid, err := wa.Client.JoinGroupWithLink(ce.Args[0]) if err != nil { ce.Reply("Failed to join group: %v", err) return @@ -303,12 +288,12 @@ func fnJoin(ce *commands.Event) { ce.Log.Debug().Stringer("group_jid", jid).Msg("User successfully joined WhatsApp group with link") ce.Reply("Successfully joined group `%s`, the portal should be created momentarily", jid) } else if strings.HasPrefix(ce.Args[0], whatsmeow.NewsletterLinkPrefix) { - info, err := wa.Client.GetNewsletterInfoWithInvite(ce.Ctx, ce.Args[0]) + info, err := wa.Client.GetNewsletterInfoWithInvite(ce.Args[0]) if err != nil { ce.Reply("Failed to get channel info: %v", err) return } - err = wa.Client.FollowNewsletter(ce.Ctx, info.ID) + err = wa.Client.FollowNewsletter(info.ID) if err != nil { ce.Reply("Failed to follow channel: %v", err) return diff --git a/pkg/connector/config.go b/pkg/connector/config.go index 2445647..aedb8f9 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -50,7 +50,6 @@ type Config struct { ForceActiveDeliveryReceipts bool `yaml:"force_active_delivery_receipts"` DirectMediaAutoRequest bool `yaml:"direct_media_auto_request"` InitialAutoReconnect bool `yaml:"initial_auto_reconnect"` - UseWhatsAppRetryStore bool `yaml:"use_whatsapp_retry_store"` AnimatedSticker msgconv.AnimatedStickerConfig `yaml:"animated_sticker"` @@ -70,8 +69,6 @@ type Config struct { RequestLocalTime int `yaml:"request_local_time"` MaxAsyncHandle int64 `yaml:"max_async_handle"` } `yaml:"media_requests"` - - BackwardsOnDemand bool `yaml:"backwards_on_demand"` } `yaml:"history_sync"` displaynameTemplate *template.Template `yaml:"-"` @@ -119,7 +116,6 @@ func upgradeConfig(helper up.Helper) { helper.Copy(up.Bool, "force_active_delivery_receipts") helper.Copy(up.Bool, "direct_media_auto_request") helper.Copy(up.Bool, "initial_auto_reconnect") - helper.Copy(up.Bool, "use_whatsapp_retry_store") helper.Copy(up.Str, "animated_sticker", "target") helper.Copy(up.Int, "animated_sticker", "args", "width") @@ -136,7 +132,6 @@ func upgradeConfig(helper up.Helper) { 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", "max_async_handle") - helper.Copy(up.Bool, "history_sync", "backwards_on_demand") } type DisplaynameParams struct { diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 91b1e11..4e50ca5 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -20,15 +20,11 @@ import ( "context" "encoding/hex" "fmt" - "net" - "net/http" "strings" "sync" "sync/atomic" - "time" "github.com/lib/pq" - "github.com/rs/zerolog" "go.mau.fi/util/dbutil" "go.mau.fi/util/random" "go.mau.fi/whatsmeow" @@ -36,19 +32,16 @@ import ( "go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/store/sqlstore" whatsmeowUpgrades "go.mau.fi/whatsmeow/store/sqlstore/upgrades" - "go.mau.fi/whatsmeow/types" waLog "go.mau.fi/whatsmeow/util/log" "google.golang.org/protobuf/proto" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/commands" - "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" "go.mau.fi/mautrix-whatsapp/pkg/connector/wadb" "go.mau.fi/mautrix-whatsapp/pkg/msgconv" - "go.mau.fi/mautrix-whatsapp/pkg/waid" ) type WhatsAppConnector struct { @@ -71,10 +64,9 @@ func init() { } var ( - _ bridgev2.NetworkConnector = (*WhatsAppConnector)(nil) - _ bridgev2.MaxFileSizeingNetwork = (*WhatsAppConnector)(nil) - _ bridgev2.StoppableNetwork = (*WhatsAppConnector)(nil) - _ bridgev2.NetworkResettingNetwork = (*WhatsAppConnector)(nil) + _ bridgev2.NetworkConnector = (*WhatsAppConnector)(nil) + _ bridgev2.MaxFileSizeingNetwork = (*WhatsAppConnector)(nil) + _ bridgev2.StoppableNetwork = (*WhatsAppConnector)(nil) ) func (wa *WhatsAppConnector) SetMaxFileSize(maxSize int64) { @@ -128,12 +120,11 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) { store.DeviceProps.Os = proto.String(wa.Config.OSName) store.DeviceProps.RequireFullSync = proto.Bool(wa.Config.HistorySync.RequestFullSync) if fsc := wa.Config.HistorySync.FullSyncConfig; fsc.DaysLimit > 0 && fsc.SizeLimit > 0 && fsc.StorageQuota > 0 { - if store.DeviceProps.HistorySyncConfig == nil { - store.DeviceProps.HistorySyncConfig = &waCompanionReg.DeviceProps_HistorySyncConfig{} + store.DeviceProps.HistorySyncConfig = &waCompanionReg.DeviceProps_HistorySyncConfig{ + FullSyncDaysLimit: proto.Uint32(fsc.DaysLimit), + FullSyncSizeMbLimit: proto.Uint32(fsc.SizeLimit), + StorageQuotaMb: proto.Uint32(fsc.StorageQuota), } - store.DeviceProps.HistorySyncConfig.FullSyncDaysLimit = proto.Uint32(fsc.DaysLimit) - store.DeviceProps.HistorySyncConfig.FullSyncSizeMbLimit = proto.Uint32(fsc.SizeLimit) - store.DeviceProps.HistorySyncConfig.StorageQuotaMb = proto.Uint32(fsc.StorageQuota) } platformID, ok := waCompanionReg.DeviceProps_PlatformType_value[strings.ToUpper(wa.Config.BrowserName)] if ok { @@ -157,80 +148,9 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error { return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"} } - if !wa.Bridge.Background && wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dms_deleted") == "false" { - wa.deleteLIDDMsMigration(ctx) - } - return nil } -func (wa *WhatsAppConnector) deleteLIDDMsMigration(ctx context.Context) { - log := zerolog.Ctx(ctx).With().Str("action", "delete lid dms").Logger() - portals, err := wa.Bridge.GetAllPortalsWithMXID(ctx) - if err != nil { - log.Err(err).Msg("Failed to get portals for LID DM deletion") - return - } - defer wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_deleted", "true") - if len(portals) == 0 { - log.Debug().Msg("No portals found") - return - } - portalsByKey := make(map[networkid.PortalKey]*bridgev2.Portal, len(portals)) - for _, p := range portals { - if p.Receiver == "" || p.RoomType != database.RoomTypeDM { - continue - } - portalsByKey[p.PortalKey] = p - } - _, err = wa.DB.Exec(ctx, "DELETE FROM whatsapp_history_sync_conversation WHERE chat_jid LIKE '%@lid'") - if err != nil { - log.Err(err).Msg("Failed to remove LID conversations from history sync") - } - for key, portal := range portalsByKey { - parsedID, err := waid.ParsePortalID(key.ID) - if err != nil { - log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to parse portal ID") - continue - } else if parsedID.Server != types.HiddenUserServer { - continue - } - var pnStr string - err = wa.DB.QueryRow(ctx, "SELECT pn FROM whatsmeow_lid_map WHERE lid=$1", parsedID.User).Scan(&pnStr) - if err != nil { - log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to get PN for LID portal") - continue - } - key.ID = waid.MakePortalID(types.JID{User: pnStr, Server: types.DefaultUserServer}) - _, pnPortalExists := portalsByKey[key] - if !pnPortalExists { - log.Warn().Str("portal_id", string(key.ID)).Msg("PN portal does not exist, not deleting LID DM") - continue - } - err = portal.Delete(ctx) - if err != nil { - log.Err(err). - Object("portal_key", portal.PortalKey). - Stringer("portal_mxid", portal.MXID). - Msg("Failed to delete LID DM portal from database") - continue - } - err = wa.Bridge.Bot.DeleteRoom(ctx, portal.MXID, false) - if err != nil { - log.Err(err). - Object("portal_key", portal.PortalKey). - Stringer("portal_mxid", portal.MXID). - Msg("Failed to delete LID DM portal from Matrix") - continue - } - log.Debug(). - Object("portal_key", portal.PortalKey). - Stringer("portal_mxid", portal.MXID). - Msg("Deleted LID DM portal") - } - log.Info().Msg("Finished deleting LID DM portals") -} - func (wa *WhatsAppConnector) Stop() { if stop := wa.stopMediaEditCacheLoop.Swap(nil); stop != nil { (*stop)() @@ -260,17 +180,8 @@ func (wa *WhatsAppConnector) onFirstBackgroundConnect() { } func (wa *WhatsAppConnector) onFirstClientConnect() { - wa.Bridge.Log.Debug().Msg("Fetching latest WhatsApp web version number") ctx := wa.Bridge.BackgroundCtx - ver, err := whatsmeow.GetLatestVersion(ctx, &http.Client{ - Transport: &http.Transport{ - DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext, - TLSHandshakeTimeout: 5 * time.Second, - ResponseHeaderTimeout: 5 * time.Second, - ForceAttemptHTTP2: true, - }, - Timeout: 10 * time.Second, - }) + ver, err := whatsmeow.GetLatestVersion(ctx, nil) if err != nil { wa.Bridge.Log.Err(err).Msg("Failed to get latest WhatsApp web version number") } else { @@ -291,13 +202,3 @@ func (wa *WhatsAppConnector) GenerateTransactionID(_ id.UserID, _ id.RoomID, _ e // so nobody can tell the difference if we just generate random bytes. return networkid.RawTransactionID(whatsmeow.WebMessageIDPrefix + strings.ToUpper(hex.EncodeToString(random.Bytes(9)))) } - -func (wa *WhatsAppConnector) ResetHTTPTransport() { - // No-op for now, whatsmeow doesn't use the shared transport config yet -} - -func (wa *WhatsAppConnector) ResetNetworkConnections() { - for _, login := range wa.Bridge.GetAllCachedUserLogins() { - login.Client.(*WhatsAppClient).Client.ResetConnection() - } -} diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index ac2fbd2..ec1610c 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -29,7 +29,6 @@ import ( "github.com/rs/zerolog" "go.mau.fi/util/exsync" - "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waMmsRetry" "go.mau.fi/whatsmeow/types/events" @@ -67,8 +66,6 @@ func (wa *WhatsAppConnector) Download(ctx context.Context, mediaID networkid.Med return wa.downloadMessageDirectMedia(ctx, parsedID, params) } else if parsedID.Avatar != nil { return wa.downloadAvatarDirectMedia(ctx, parsedID, params) - } else if parsedID.Sticker != nil { - return wa.downloadStickerDirectMedia(ctx, parsedID, params) } else { return nil, fmt.Errorf("unexpected media ID parsing result") } @@ -88,24 +85,17 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars return nil, fmt.Errorf("failed to get avatar cache entry: %w", err) } if cachedInfo != nil && cachedInfo.Gone { - return nil, mautrix.MNotFound.WithMessage("Avatar is no longer available (cached response)") + return nil, mautrix.MNotFound.WithMessage("Avatar is no longer available") } else if cachedInfo == nil || cachedInfo.Expiry.Time.Before(time.Now().Add(5*time.Minute)) { zerolog.Ctx(ctx).Debug(). Str("avatar_id", parsedID.Avatar.AvatarID). Msg("Refreshing avatar URL from WhatsApp servers") - avatar, err := waClient.Client.GetProfilePictureInfo(ctx, parsedID.Avatar.TargetJID, &whatsmeow.GetProfilePictureParams{ + avatar, err := waClient.Client.GetProfilePictureInfo(parsedID.Avatar.TargetJID, &whatsmeow.GetProfilePictureParams{ IsCommunity: parsedID.Avatar.Community, }) if errors.Is(err, whatsmeow.ErrProfilePictureNotSet) || errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) || (err == nil && (avatar == nil || avatar.ID != parsedID.Avatar.AvatarID)) { - zerolog.Ctx(ctx).Debug(). - Err(err). - Stringer("target_jid", parsedID.Avatar.TargetJID). - Bool("is_community", parsedID.Avatar.Community). - Str("wanted_avatar_id", parsedID.Avatar.AvatarID). - Str("got_avatar_id", ptr.Val(avatar).ID). - Msg("Avatar is no longer available") err = wa.DB.AvatarCache.Put(ctx, &wadb.AvatarCacheEntry{ EntityJID: parsedID.Avatar.TargetJID, AvatarID: parsedID.Avatar.AvatarID, @@ -118,7 +108,7 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars } return nil, mautrix.MNotFound.WithMessage("Avatar is no longer available") } else if err != nil { - return nil, mautrix.MUnknown.WithMessage("failed to refresh avatar url: %w", err).WithCanRetry(true) + return nil, fmt.Errorf("failed to refresh avatar url: %w", err) } cachedInfo = avatarInfoToCacheEntry(ctx, parsedID.Avatar.TargetJID, avatar) err = wa.DB.AvatarCache.Put(ctx, cachedInfo) @@ -129,33 +119,17 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars } } return &mediaproxy.GetMediaResponseFile{ - Callback: func(w *os.File) (*mediaproxy.FileMeta, error) { - return &mediaproxy.FileMeta{}, waClient.Client.DownloadMediaWithPathToFile( + Callback: func(w *os.File) error { + return waClient.Client.DownloadMediaWithPathToFile( ctx, cachedInfo.DirectPath, nil, nil, nil, 0, "", "", w, ) }, + ContentType: "", // TODO are avatars always jpeg? }, 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) { + log := zerolog.Ctx(ctx) msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String()) if err != nil { return nil, fmt.Errorf("failed to get message: %w", err) @@ -187,38 +161,25 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par } } if ul == nil || !ul.Client.IsLoggedIn() { - return nil, bridgev2.ErrNotLoggedIn + return nil, fmt.Errorf("no logged in user found") } waClient := ul.Client.(*WhatsAppClient) if waClient.Client == nil { 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{ - Callback: func(f *os.File) (*mediaproxy.FileMeta, error) { - log := zerolog.Ctx(ctx) - err := waClient.Client.DownloadToFile(ctx, dm, f) - if keys != nil && (errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent)) { + Callback: func(f *os.File) error { + err := waClient.Client.DownloadToFile(ctx, keys, f) + if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) { val := params["fi.mau.whatsapp.reload_media"] if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") { - return nil, ErrReloadNeeded + return ErrReloadNeeded } 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 { log.Trace().Err(err).Msg("Failed to wait for media for direct download") - return nil, err + return err } log.Trace().Msg("Retrying download after successful retry") err = waClient.Client.DownloadToFile(ctx, keys, f) @@ -226,28 +187,27 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse( 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") } else if err != nil { - return nil, err + return err } - if mimeType == "application/was" { + if keys.MimeType == "application/was" { if _, err := f.Seek(0, io.SeekStart); err != nil { - return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err) + return fmt.Errorf("failed to seek to start of sticker zip: %w", err) } else if zipData, err := io.ReadAll(f); err != nil { - return nil, fmt.Errorf("failed to read sticker zip: %w", err) - } else if data, _, err := msgconv.ExtractAnimatedSticker(zipData); err != nil { - return nil, fmt.Errorf("failed to extract animated sticker: %w %x", err, zipData) + return fmt.Errorf("failed to read sticker zip: %w", err) + } else if data, err := msgconv.ExtractAnimatedSticker(zipData); err != nil { + return fmt.Errorf("failed to extract animated sticker: %w %x", err, zipData) } else if _, err := f.WriteAt(data, 0); err != nil { - return nil, fmt.Errorf("failed to write animated sticker to file: %w", err) + return fmt.Errorf("failed to write animated sticker to file: %w", err) } else if err := f.Truncate(int64(len(data))); err != nil { - return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err) + return fmt.Errorf("failed to truncate animated sticker file: %w", err) } - mimeType = "video/lottie+json" } - return &mediaproxy.FileMeta{ - ContentType: mimeType, - }, nil + return nil }, + // TODO? + ContentType: "", }, nil } @@ -285,16 +245,12 @@ func (wa *WhatsAppClient) requestAndWaitDirectMedia(ctx context.Context, rawMsgI } switch state.resultType { case waMmsRetry.MediaRetryNotification_NOT_FOUND: - return mautrix.MNotFound.WithMessage("This media was not found on your phone.") - case waMmsRetry.MediaRetryNotification_DECRYPTION_ERROR: - return mautrix.MNotFound.WithMessage("Unable to retrieve media: phone reported a decryption error. The original message may have been deleted.") - case waMmsRetry.MediaRetryNotification_GENERAL_ERROR: - return mautrix.MNotFound.WithMessage("Unable to retrieve media: phone returned an error. Please ensure your phone is connected to the internet and WhatsApp is running.").WithCanRetry(true) + return mautrix.MNotFound.WithMessage("Media not found on phone") default: - return mautrix.MNotFound.WithMessage(fmt.Sprintf("Unable to retrieve media: phone returned error code %d", state.resultType)).WithCanRetry(true) + return mautrix.MNotFound.WithMessage("Phone returned error response") } case <-time.After(30 * time.Second): - return mautrix.MNotFound.WithMessage("Phone did not respond in time. Please ensure your phone is connected to the internet and WhatsApp is open.").WithStatus(http.StatusGatewayTimeout).WithCanRetry(true) + return mautrix.MNotFound.WithMessage("Phone did not respond in time").WithStatus(http.StatusGatewayTimeout) case <-ctx.Done(): return ctx.Err() } @@ -306,7 +262,7 @@ func (wa *WhatsAppClient) requestDirectMedia(ctx context.Context, rawMsgID netwo defer state.Unlock() if !state.requested { zerolog.Ctx(ctx).Debug().Msg("Sending request for missing media in direct download") - err := wa.sendMediaRequestDirect(ctx, rawMsgID, key) + err := wa.sendMediaRequestDirect(rawMsgID, key) if err != nil { return nil, fmt.Errorf("failed to send media retry request: %w", err) } diff --git a/pkg/connector/events.go b/pkg/connector/events.go index de5bdb8..f371cc0 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -133,21 +133,13 @@ func (evt *WAMessageEvent) PreHandle(ctx context.Context, portal *bridgev2.Porta return } meta := portal.Metadata.(*waid.PortalMetadata) - if meta.AddressingMode == types.AddressingModeLID && evt.Info.Sender.Server == types.DefaultUserServer { - evt.Info.Sender, evt.Info.SenderAlt = evt.Info.SenderAlt, evt.Info.Sender - zerolog.Ctx(ctx).Debug(). - Stringer("lid", evt.Info.Sender). - Stringer("pn", evt.Info.SenderAlt). - Str("message_id", evt.Info.ID). - Msg("Forced phone number sender to LID in group message") - } if meta.AddressingMode == types.AddressingModeLID || meta.LIDMigrationAttempted { return } log := zerolog.Ctx(ctx).With().Str("action", "group lid migration").Logger() ctx = log.WithContext(ctx) meta.LIDMigrationAttempted = true - info, err := evt.wa.Client.GetGroupInfo(ctx, portalJID) + info, err := evt.wa.Client.GetGroupInfo(portalJID) if err != nil { log.Err(err).Msg("Failed to get group info for lid migration") return @@ -217,15 +209,9 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por func (evt *WAMessageEvent) GetTargetMessage() networkid.MessageID { if reactionMsg := evt.Message.GetReactionMessage(); reactionMsg != nil { - ctx := evt.wa.UserLogin.Log. - With().Str("action", "get reaction target message").Str("message_id", evt.Info.ID).Logger(). - WithContext(evt.wa.Main.Bridge.BackgroundCtx) - return msgconv.KeyToMessageID(ctx, evt.wa.Client, evt.Info.Chat, evt.Info.Sender, reactionMsg.GetKey()) + return msgconv.KeyToMessageID(evt.wa.Client, evt.Info.Chat, evt.Info.Sender, reactionMsg.GetKey()) } else if protocolMsg := evt.Message.GetProtocolMessage(); protocolMsg != nil { - ctx := evt.wa.UserLogin.Log. - With().Str("action", "get edit target message").Str("message_id", evt.Info.ID).Logger(). - WithContext(evt.wa.Main.Bridge.BackgroundCtx) - return msgconv.KeyToMessageID(ctx, evt.wa.Client, evt.Info.Chat, evt.Info.Sender, protocolMsg.GetKey()) + return msgconv.KeyToMessageID(evt.wa.Client, evt.Info.Chat, evt.Info.Sender, protocolMsg.GetKey()) } return "" } diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index 564f25e..99162b3 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -64,11 +64,6 @@ force_active_delivery_receipts: false direct_media_auto_request: true # Should the bridge automatically reconnect if it fails to connect on startup? initial_auto_reconnect: true -# WhatsApp messages are sometimes undecryptable. Should the bridge store messages it sends in the -# bridge database in order to accept retry receipts from other WhatsApp users for messages sent via -# the bridge? By default, the bridge only stores messages in memory, and therefore can't accept -# retry receipts if the bridge is restarted after the message is sent. -use_whatsapp_retry_store: false # Settings for converting animated stickers. animated_sticker: @@ -121,6 +116,3 @@ history_sync: request_local_time: 120 # Maximum number of media request responses to handle in parallel per user. 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 diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index b0963ec..fe77c37 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -107,13 +107,12 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID) msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID)) 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) if err != nil { return nil, err } var pickedMessageID networkid.MessageID - if resp.Sender == wa.GetStore().GetLID() && chatJID.Server != types.DefaultUserServer { + if resp.Sender == wa.GetStore().GetLID() { pickedMessageID = wrappedMsgID2 msg.RemovePending(networkid.TransactionID(wrappedMsgID)) } else { @@ -323,7 +322,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt * messagesToRead[key] = append(messagesToRead[key], parsed.ID) } for messageSender, ids := range messagesToRead { - err = wa.Client.MarkRead(ctx, ids, receipt.Receipt.Timestamp, portalJID, messageSender) + err = wa.Client.MarkRead(ids, receipt.Receipt.Timestamp, portalJID, messageSender) if err != nil { log.Err(err).Strs("ids", ids).Msg("Failed to mark messages as read") } @@ -353,12 +352,12 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2. } if wa.Main.Config.SendPresenceOnTyping { - err = wa.updatePresence(ctx, types.PresenceAvailable) + err = wa.updatePresence(types.PresenceAvailable) if err != nil { zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set presence on typing") } } - return wa.Client.SendChatPresence(ctx, portalJID, chatPresence, mediaPresence) + return wa.Client.SendChatPresence(portalJID, chatPresence, mediaPresence) } var errUnsupportedDisappearingTimer = bridgev2.WrapErrorInStatus(errors.New("invalid value for disappearing timer")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true) @@ -376,7 +375,7 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg } settingTS := time.UnixMilli(msg.Event.Timestamp) - err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS) + err = wa.Client.SetDisappearingTimer(portalJID, msg.Content.Timer.Duration, settingTS) if err != nil { return false, err } @@ -391,22 +390,18 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg return true, nil } -func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) { - if msg.Type.IsSelf && msg.OrigSender != nil { - return nil, nil - } - +func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (bool, error) { portalJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { - return nil, err + return false, err } if msg.Portal.RoomType == database.RoomTypeDM { switch msg.Type { case bridgev2.Invite: - return nil, fmt.Errorf("cannot invite additional user to dm") + return false, fmt.Errorf("cannot invite additional user to dm") default: - return nil, nil + return false, nil } } @@ -419,7 +414,7 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg case bridgev2.Leave, bridgev2.Kick: action = whatsmeow.ParticipantChangeRemove default: - return nil, nil + return false, nil } switch target := msg.Target.(type) { @@ -428,26 +423,19 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg case *bridgev2.UserLogin: ghost, err := target.Bridge.GetGhostByID(ctx, networkid.UserID(target.ID)) if err != nil { - return nil, fmt.Errorf("failed to get ghost for user: %w", err) + return false, fmt.Errorf("failed to get ghost for user: %w", err) } changes[0] = waid.ParseUserID(ghost.ID) default: - return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target) + return false, fmt.Errorf("cannot get target intent: unknown type: %T", target) } - resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action) + _, err = wa.Client.UpdateGroupParticipants(portalJID, changes, action) if err != nil { - return nil, err - } else if len(resp) == 0 { - return nil, fmt.Errorf("no response for participant change") - } else if resp[0].Error != 0 { - return nil, fmt.Errorf("failed to change participant: code %d", resp[0].Error) + return false, err } - zerolog.Ctx(ctx).Debug(). - Any("change_response", resp). - Msg("Handled membership change") - return &bridgev2.MatrixMembershipResult{RedirectTo: waid.MakeUserID(resp[0].JID)}, nil + return true, nil } func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (bool, error) { @@ -460,7 +448,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev return false, fmt.Errorf("cannot set room name for DM") } - err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name) + err = wa.Client.SetGroupName(portalJID, msg.Content.Name) if err != nil { return false, err } @@ -483,7 +471,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge newID := wa.Client.GenerateMessageID() oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID - err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic) + err = wa.Client.SetGroupTopic(portalJID, oldID, newID, msg.Content.Topic) if err != nil { return false, err } @@ -506,8 +494,8 @@ func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridg } var data []byte - if msg.Content.URL != "" { - data, err = msg.Portal.Bridge.Bot.DownloadMedia(ctx, msg.Content.URL, nil) + if msg.Content.URL != "" || msg.Content.MSC3414File != nil { + data, err = msg.Portal.Bridge.Bot.DownloadMedia(ctx, msg.Content.URL, msg.Content.MSC3414File) if err != nil { return false, fmt.Errorf("failed to download avatar: %w", err) } @@ -518,7 +506,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridg } } - avatarID, err := wa.Client.SetGroupPhoto(ctx, portalJID, data) + avatarID, err := wa.Client.SetGroupPhoto(portalJID, data) if err != nil { return false, err } @@ -652,21 +640,12 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg if err != nil { return err } - if chatJID.Server == types.GroupServer { - memberInfo, err := wa.Main.Bridge.Matrix.GetMemberInfo(ctx, msg.Portal.MXID, wa.UserLogin.UserMXID) - if err != nil { - return fmt.Errorf("failed to get own member info: %w", err) - } else if memberInfo.Membership == event.MembershipJoin { - err = wa.Client.LeaveGroup(ctx, chatJID) - if err != nil { - // TODO ignore errors saying you already left the group? - return fmt.Errorf("failed to leave group before deleting chat: %w", err) - } - } - } lastTS, lastKey, err := wa.getLastMessageInfo(ctx, chatJID, msg.Portal.PortalKey) if err != nil { return err } - return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true)) + if lastKey == nil { + return fmt.Errorf("failed to delete chat: no messages found") + } + return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey)) } diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 3abcede..4429d05 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -25,10 +25,8 @@ import ( "github.com/rs/zerolog" "go.mau.fi/util/ptr" - "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/appstate" "go.mau.fi/whatsmeow/proto/waE2E" - "go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" "maunium.net/go/mautrix/bridgev2" @@ -100,9 +98,9 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { case *events.MarkChatAsRead: success = wa.handleWAMarkChatAsRead(ctx, evt) case *events.DeleteForMe: - success = wa.handleWADeleteForMe(ctx, evt) + success = wa.handleWADeleteForMe(evt) case *events.DeleteChat: - success = wa.handleWADeleteChat(ctx, evt) + success = wa.handleWADeleteChat(evt) case *events.Mute: success = wa.handleWAMute(evt) case *events.Archive: @@ -111,7 +109,9 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { success = wa.handleWAPin(evt) case *events.HistorySync: - wa.UserLogin.Log.Warn().Msg("Unexpected history sync event received") + if wa.Main.Bridge.Config.Backfill.Enabled { + wa.historySyncs <- evt.Data + } case *events.MediaRetry: wa.phoneSeen(evt.Timestamp) success = wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{MediaRetry: evt, wa: wa}).Success @@ -128,15 +128,21 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { success = wa.handleWAPictureUpdate(ctx, evt) case *events.AppStateSyncComplete: - wa.handleWAAppStateSyncComplete(ctx, evt) - case *events.AppStateSyncError: - wa.handleWAAppStateSyncError(ctx, evt) + if len(wa.GetStore().PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock { + err := wa.updatePresence(types.PresenceUnavailable) + if err != nil { + log.Warn().Err(err).Msg("Failed to send presence after app state sync") + } + go wa.syncRemoteProfile(log.WithContext(context.Background()), nil) + } else if evt.Name == appstate.WAPatchCriticalUnblockLow { + go wa.resyncContacts(false, true) + } case *events.AppState: // Intentionally ignored case *events.PushNameSetting: // Send presence available when connecting and when the pushname is changed. // This makes sure that outgoing messages always have the right pushname. - err := wa.updatePresence(ctx, types.PresenceUnavailable) + err := wa.updatePresence(types.PresenceUnavailable) if err != nil { log.Warn().Err(err).Msg("Failed to send presence after push name update") } @@ -144,10 +150,6 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { if err != nil { log.Err(err).Msg("Failed to update push name in store") } - _, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.GetStore().GetLID().ToNonAD(), evt.Action.GetName()) - if err != nil { - log.Err(err).Msg("Failed to update push name in store") - } go wa.syncGhost(wa.JID.ToNonAD(), "push name setting", nil) case *events.Contact: go wa.syncGhost(evt.JID, "contact event", nil) @@ -161,14 +163,13 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) if len(wa.GetStore().PushName) > 0 { go func() { - err := wa.updatePresence(ctx, types.PresenceUnavailable) + err := wa.updatePresence(types.PresenceUnavailable) if err != nil { log.Warn().Err(err).Msg("Failed to send initial presence after connecting") } }() - go wa.syncRemoteProfile(ctx, nil) + go wa.syncRemoteProfile(log.WithContext(context.Background()), nil) } - wa.MC.OnConnect(store.GetWAVersion()[2], wa.Device.Platform) case *events.OfflineSyncPreview: log.Info(). Int("message_count", evt.Messages). @@ -246,20 +247,12 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { return } -func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string, info *types.MessageSource, msgID any) { - if (info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BroadcastServer) && - info.Sender.Server == types.HiddenUserServer && info.SenderAlt.IsEmpty() { - info.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Sender) - } - if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() { - info.RecipientAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat) - } +func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, info *types.MessageInfo) { if info.Chat.Server == types.HiddenUserServer && info.Sender.ToNonAD() == info.Chat && info.SenderAlt.Server == types.DefaultUserServer { wa.UserLogin.Log.Debug(). Stringer("lid", info.Sender). Stringer("pn", info.SenderAlt). - Any("message_id", msgID). - Str("evt_type", evtType). + Str("message_id", info.ID). Msg("Forced LID DM sender to phone number in incoming message") info.Sender, info.SenderAlt = info.SenderAlt, info.Sender info.Chat = info.Sender.ToNonAD() @@ -267,40 +260,21 @@ func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string, wa.UserLogin.Log.Debug(). Stringer("lid", info.Chat). Stringer("pn", info.RecipientAlt). - Any("message_id", msgID). - Str("evt_type", evtType). + Str("message_id", info.ID). Msg("Forced LID DM sender to phone number in own message sent from another device") info.Chat = info.RecipientAlt.ToNonAD() - if info.Sender.Server == types.HiddenUserServer { - info.Sender, info.SenderAlt = info.SenderAlt, info.Sender - if info.Sender.IsEmpty() { - info.Sender = wa.GetStore().GetJID() - info.Sender.Device = info.SenderAlt.Device - } - } - } else if info.Chat.Server == types.BroadcastServer && info.Sender.Server == types.HiddenUserServer && info.SenderAlt.Server == types.DefaultUserServer { - wa.UserLogin.Log.Debug(). - Stringer("lid", info.Sender). - Stringer("pn", info.SenderAlt). - Stringer("chat", info.Chat). - Any("message_id", msgID). - Str("evt_type", evtType). - Msg("Forced LID broadcast list sender to phone number in incoming message") - info.Sender, info.SenderAlt = info.SenderAlt, info.Sender } else if info.Sender.Server == types.BotServer && info.Chat.Server == types.HiddenUserServer { chatPN, err := wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat) if err != nil { wa.UserLogin.Log.Err(err). - Any("message_id", msgID). + Str("message_id", info.ID). Stringer("lid", info.Chat). - Str("evt_type", evtType). Msg("Failed to get phone number of DM for incoming bot message") } else if !chatPN.IsEmpty() { wa.UserLogin.Log.Debug(). Stringer("lid", info.Chat). Stringer("pn", chatPN). - Any("message_id", msgID). - Str("evt_type", evtType). + Str("message_id", info.ID). Msg("Forced LID chat to phone number in bot message") info.Chat = chatPN } @@ -309,10 +283,45 @@ func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string, func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) { success = true + wa.rerouteWAMessage(ctx, &evt.Info) + wa.UserLogin.Log.Trace(). + Any("info", evt.Info). + Any("payload", evt.Message). + Msg("Received WhatsApp message") if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast { return } + if evt.Info.IsFromMe && + evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil && + wa.Main.Bridge.Config.Backfill.Enabled && + wa.Client.ManualHistorySyncDownload { + wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification) + } + + messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation() + if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD { + parentKey := messageAssoc.GetParentMessageKey() + associatedMessage := evt.Message.GetAssociatedChildMessage().GetMessage() + wa.UserLogin.Log.Debug(). + Str("message_id", evt.Info.ID). + Str("parent_id", parentKey.GetID()). + Stringer("assoc_type", assocType). + Msg("Received HD replacement message, converting to edit") + + protocolMsg := &waE2E.ProtocolMessage{ + Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(), + Key: parentKey, + EditedMessage: associatedMessage, + } + evt.Message = &waE2E.Message{ + ProtocolMessage: protocolMsg, + } + } + 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 { @@ -334,58 +343,13 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa 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") + 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) } - wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID) - wa.UserLogin.Log.Trace(). - Any("info", evt.Info). - Any("payload", evt.Message). - Msg("Received WhatsApp message") - if evt.Info.IsFromMe && - evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil && - wa.Main.Bridge.Config.Backfill.Enabled { - wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification) - } - if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") { - return - } - - messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation() - if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD { - parentKey := messageAssoc.GetParentMessageKey() - associatedMessage := evt.Message.GetAssociatedChildMessage().GetMessage() - wa.UserLogin.Log.Debug(). - Str("message_id", evt.Info.ID). - Str("parent_id", parentKey.GetID()). - Stringer("assoc_type", assocType). - Msg("Received HD replacement message, converting to edit") - - protocolMsg := &waE2E.ProtocolMessage{ - Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(), - Key: parentKey, - EditedMessage: associatedMessage, - } - evt.Message = &waE2E.Message{ - ProtocolMessage: protocolMsg, - } - } else if assocType == waE2E.MessageAssociation_MOTION_PHOTO { - //evt.Message = evt.Message.GetAssociatedChildMessage().GetMessage() - wa.UserLogin.Log.Debug(). - Str("message_id", evt.Info.ID). - Str("parent_id", messageAssoc.GetParentMessageKey().GetID()). - Msg("Ignoring motion photo update") - return - } - res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{ MessageInfoWrapper: &MessageInfoWrapper{ Info: evt.Info, @@ -400,7 +364,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa } func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool { - wa.rerouteWAMessage(ctx, "undecryptable message", &evt.Info.MessageSource, evt.Info.ID) + wa.rerouteWAMessage(ctx, &evt.Info) wa.UserLogin.Log.Debug(). Any("info", evt.Info). Bool("unavailable", evt.IsUnavailable). @@ -424,8 +388,22 @@ func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt } func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Receipt) (success bool) { - origChat := evt.Chat - wa.rerouteWAMessage(ctx, "receipt", &evt.MessageSource, evt.MessageIDs) + if evt.Chat.Server == types.HiddenUserServer && evt.Sender.ToNonAD() == evt.Chat && evt.SenderAlt.Server == types.DefaultUserServer { + wa.UserLogin.Log.Debug(). + Stringer("lid", evt.Sender). + Stringer("pn", evt.SenderAlt). + Strs("message_id", evt.MessageIDs). + Msg("Forced LID DM sender to phone number in incoming receipt") + evt.Sender, evt.SenderAlt = evt.SenderAlt, evt.Sender + evt.Chat = evt.Sender.ToNonAD() + } else if evt.Chat.Server == types.HiddenUserServer && evt.IsFromMe && evt.RecipientAlt.Server == types.DefaultUserServer { + wa.UserLogin.Log.Debug(). + Stringer("lid", evt.Chat). + Stringer("pn", evt.RecipientAlt). + Strs("message_id", evt.MessageIDs). + Msg("Forced LID DM sender to phone number in own receipt sent from another device") + evt.Chat = evt.RecipientAlt.ToNonAD() + } if evt.IsFromMe && evt.Sender.Device == 0 { wa.phoneSeen(evt.Timestamp) } @@ -444,10 +422,6 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei messageSender := wa.JID if !evt.MessageSender.IsEmpty() { messageSender = evt.MessageSender - // Second part of rerouting receipts in LID chats - if messageSender == origChat && evt.Chat != origChat { - messageSender = evt.Chat - } } else if evt.Chat.Server == types.GroupServer && evt.Sender.Server == types.HiddenUserServer { lid := wa.GetStore().GetLID() if !lid.IsEmpty() { @@ -470,15 +444,6 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei } func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events.ChatPresence) { - if evt.Chat.Server == types.HiddenUserServer && evt.Sender.ToNonAD() == evt.Chat { - if evt.SenderAlt.IsEmpty() { - evt.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, evt.Sender) - } - if evt.SenderAlt.Server == types.DefaultUserServer { - evt.Sender, evt.SenderAlt = evt.SenderAlt, evt.Sender - evt.Chat = evt.Sender.ToNonAD() - } - } typingType := bridgev2.TypingTypeText timeout := 15 * time.Second if evt.Media == types.ChatPresenceMediaAudio { @@ -508,7 +473,7 @@ func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onC } else if reason == events.ConnectFailureMainDeviceGone { errorCode = WAMainDeviceGone } - wa.Disconnect() + wa.Client.Disconnect() wa.Client = nil wa.JID = types.EmptyJID wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0 @@ -544,7 +509,6 @@ func (wa *WhatsAppClient) handleWACallStart(ctx context.Context, group, sender, Sender: wa.makeEventSender(ctx, sender), CreatePortal: true, Timestamp: ts, - StreamOrder: ts.Unix(), }, Data: callType, ID: waid.MakeFakeMessageID(chat, sender, "call-"+id), @@ -563,10 +527,6 @@ func convertCallStart(ctx context.Context, portal *bridgev2.Portal, intent bridg Content: &event.MessageEventContent{ MsgType: event.MsgText, Body: text, - BeeperActionMessage: &event.BeeperActionMessage{ - Type: event.BeeperActionMessageCall, - CallType: event.BeeperActionMessageCallType(callType), - }, }, }}, }, nil @@ -611,12 +571,11 @@ func convertIdentityChange(ctx context.Context, portal *bridgev2.Portal, intent }, nil } -func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.DeleteChat) bool { - chatJID := wa.maybeConvertJIDToLID(ctx, evt.JID) +func (wa *WhatsAppClient) handleWADeleteChat(evt *events.DeleteChat) bool { return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventChatDelete, - PortalKey: wa.makeWAPortalKey(chatJID), + PortalKey: wa.makeWAPortalKey(evt.JID), Timestamp: evt.Timestamp, }, OnlyForMe: true, @@ -624,25 +583,23 @@ func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.De }).Success } -func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool { - chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID) +func (wa *WhatsAppClient) handleWADeleteForMe(evt *events.DeleteForMe) bool { return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventMessageRemove, - PortalKey: wa.makeWAPortalKey(chatJID), + PortalKey: wa.makeWAPortalKey(evt.ChatJID), Timestamp: evt.Timestamp, }, - TargetMessage: waid.MakeMessageID(chatJID, evt.SenderJID, evt.MessageID), + TargetMessage: waid.MakeMessageID(evt.ChatJID, evt.SenderJID, evt.MessageID), OnlyForMe: true, }).Success } func (wa *WhatsAppClient) handleWAMarkChatAsRead(ctx context.Context, evt *events.MarkChatAsRead) bool { - chatJID := wa.maybeConvertJIDToLID(ctx, evt.JID) return wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventReadReceipt, - PortalKey: wa.makeWAPortalKey(chatJID), + PortalKey: wa.makeWAPortalKey(evt.JID), Sender: wa.makeEventSender(ctx, wa.JID), Timestamp: evt.Timestamp, }, @@ -818,99 +775,3 @@ func (wa *WhatsAppClient) handleWAPin(evt *events.Pin) bool { Tag: &tag, }) } - -func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt *events.AppStateSyncComplete) { - log := zerolog.Ctx(ctx).With(). - Str("patch_name", string(evt.Name)). - Uint64("patch_version", evt.Version). - Logger() - if len(wa.GetStore().PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock { - err := wa.updatePresence(ctx, types.PresenceUnavailable) - if err != nil { - log.Warn().Err(err).Msg("Failed to send presence after app state sync") - } - go wa.syncRemoteProfile(log.WithContext(context.Background()), nil) - } else if evt.Name == appstate.WAPatchCriticalUnblockLow { - go wa.resyncContacts(false, true) - } - wa.appStateRecoveryLock.Lock() - defer wa.appStateRecoveryLock.Unlock() - meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata) - if ts, exists := meta.AppStateRecoveryAttempted[evt.Name]; exists { - delete(wa.appStateFullSyncAttempted, evt.Name) - delete(meta.AppStateRecoveryAttempted, evt.Name) - err := wa.UserLogin.Save(ctx) - if err != nil { - log.Err(err).Msg("Failed to save login metadata after unmarking app state recovery as attempted") - } else { - log.Info(). - Time("recovery_ts", ts). - Msg("Unmarked app state recovery as attempted after successful full sync") - } - } else if ts, exists = wa.appStateFullSyncAttempted[evt.Name]; exists { - delete(wa.appStateFullSyncAttempted, evt.Name) - log.Debug().Time("full_sync_ts", ts).Msg("Unmarked app state full sync attempted after successful sync") - } -} - -func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *events.AppStateSyncError) { - log := zerolog.Ctx(ctx).With(). - Str("patch_name", string(evt.Name)). - Logger() - wa.appStateRecoveryLock.Lock() - defer wa.appStateRecoveryLock.Unlock() - meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata) - lastRecovery := meta.AppStateRecoveryAttempted[evt.Name] - lastFullSync := wa.appStateFullSyncAttempted[evt.Name] - if !lastRecovery.IsZero() && time.Since(lastRecovery) < 48*time.Hour { - log.Debug().Err(evt.Error). - Time("last_recovery_attempt", lastRecovery). - Time("last_full_sync_attempt", lastFullSync). - Msg("App state sync failed, but recovery already attempted") - return - } - if !evt.FullSync { - if !lastFullSync.IsZero() { - log.Debug(). - Err(evt.Error). - Time("last_full_sync_attempt", lastFullSync). - Msg("App state sync failed, but full sync already attempted") - return - } - wa.appStateFullSyncAttempted[evt.Name] = time.Now() - log.Info(). - Err(evt.Error). - Msg("Trying full sync for app state after partial sync error") - go func() { - err := wa.Client.FetchAppState(ctx, evt.Name, true, false) - if err != nil { - log.Err(err).Msg("Full app state sync failed") - } else { - log.Debug().Msg("Full app state sync succeeded") - } - }() - return - } - log.Info(). - Err(evt.Error). - Msg("Trying recovery for app state after full sync error") - if meta.AppStateRecoveryAttempted == nil { - meta.AppStateRecoveryAttempted = make(map[appstate.WAPatchName]time.Time) - } - meta.AppStateRecoveryAttempted[evt.Name] = time.Now() - err := wa.UserLogin.Save(ctx) - if err != nil { - log.Err(err).Msg("Failed to save login metadata after marking app state recovery as attempted") - } - go func() { - resp, err := wa.Client.SendPeerMessage(ctx, whatsmeow.BuildAppStateRecoveryRequest(evt.Name)) - if err != nil { - log.Err(err).Msg("Failed to send app state recovery request") - } else { - log.Debug(). - Str("message_id", resp.ID). - Time("message_ts", resp.Timestamp). - Msg("Sent app state recovery request") - } - }() -} diff --git a/pkg/connector/id.go b/pkg/connector/id.go index c07e431..5028b7b 100644 --- a/pkg/connector/id.go +++ b/pkg/connector/id.go @@ -68,16 +68,3 @@ func (wa *WhatsAppClient) messageIDToKey(id *waid.ParsedMessageID) *waCommon.Mes } return key } - -func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, chatJID types.JID) types.JID { - if chatJID.Server == types.HiddenUserServer { - if pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, chatJID); err != nil { - wa.UserLogin.Log.Err(err). - Stringer("lid", chatJID). - Msg("Failed to get phone number for LID chat") - } else if !pn.IsEmpty() { - return pn.ToNonAD() - } - } - return chatJID -} diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 88e7b75..2442356 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -74,11 +74,6 @@ var ( Err: "Phone number must be in international format", StatusCode: http.StatusBadRequest, } - ErrRateLimitedByWhatsApp = bridgev2.RespError{ - ErrCode: "FI.MAU.WHATSAPP.RATE_LIMITED", - Err: "Rate limited by WhatsApp", - StatusCode: http.StatusTooManyRequests, - } ) func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) { @@ -201,8 +196,6 @@ func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) return nil, ErrPhoneNumberTooShort } else if errors.Is(err, whatsmeow.ErrPhoneNumberIsNotInternational) { return nil, ErrPhoneNumberIsNotInternational - } else if errors.Is(err, whatsmeow.ErrIQRateOverLimit) { - return nil, ErrRateLimitedByWhatsApp } return nil, err } @@ -359,7 +352,7 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { } ul.Client.(*WhatsAppClient).isNewLogin = true - ul.Client.Connect(ul.Log.WithContext(wl.Main.Bridge.BackgroundCtx)) + ul.Client.Connect(ul.Log.WithContext(context.Background())) return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeComplete, diff --git a/pkg/connector/mclient.go b/pkg/connector/mclient.go deleted file mode 100644 index 0930617..0000000 --- a/pkg/connector/mclient.go +++ /dev/null @@ -1,81 +0,0 @@ -// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2026 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package connector - -import ( - "context" - "encoding/json" - "time" - - "go.mau.fi/whatsmeow" - waBinary "go.mau.fi/whatsmeow/binary" - "go.mau.fi/whatsmeow/types" - - "go.mau.fi/mautrix-whatsapp/pkg/waid" -) - -type NewMCFunc = func(json.RawMessage, mWAClient) mClient - -var NewMC NewMCFunc - -func (wa *WhatsAppClient) initMC() { - if NewMC != nil { - wa.MC = NewMC(wa.UserLogin.Metadata.(*waid.UserLoginMetadata).MData, wa) - } -} - -type mClient = interface { - OnConnect(version uint32, platform string) -} - -type noopMC struct{} - -var noopMCInstance mClient = &noopMC{} - -func (n *noopMC) OnConnect(version uint32, platform string) {} - -type mWAClient = interface { - MSend(data []byte) - MSave(data json.RawMessage) -} - -var _ mWAClient = (*WhatsAppClient)(nil) - -// Deprecated: ignore DangerousInternal error -func (wa *WhatsAppClient) MSend(bytes []byte) { - _, err := wa.Client.DangerousInternals().SendIQAsync(wa.Main.Bridge.BackgroundCtx, whatsmeow.DangerousInfoQuery{ - Namespace: "w:stats", - Type: "set", - To: types.ServerJID, - Content: []waBinary.Node{{ - Tag: "add", - Attrs: waBinary.Attrs{"t": time.Now().Unix()}, - Content: bytes, - }}, - }) - if err != nil { - wa.UserLogin.Log.Err(err).Msg("Failed to send stats") - } -} - -func (wa *WhatsAppClient) MSave(s json.RawMessage) { - wa.UserLogin.Metadata.(*waid.UserLoginMetadata).MData = s - err := wa.UserLogin.Save(context.Background()) - if err != nil { - wa.UserLogin.Log.Err(err).Msg("Failed to save MC data") - } -} diff --git a/pkg/connector/mediarequest.go b/pkg/connector/mediarequest.go index 2c382d1..e0f4c5b 100644 --- a/pkg/connector/mediarequest.go +++ b/pkg/connector/mediarequest.go @@ -137,7 +137,7 @@ func (wa *WhatsAppClient) sendMediaRequest(ctx context.Context, req *wadb.MediaR req.Status = wadb.MediaBackfillRequestStatusRequestSkipped return } - err = wa.sendMediaRequestDirect(ctx, req.MessageID, req.MediaKey) + err = wa.sendMediaRequestDirect(req.MessageID, req.MediaKey) if err != nil { log.Err(err).Msg("Failed to send media retry request") req.Status = wadb.MediaBackfillRequestStatusRequestFailed @@ -148,12 +148,12 @@ func (wa *WhatsAppClient) sendMediaRequest(ctx context.Context, req *wadb.MediaR } } -func (wa *WhatsAppClient) sendMediaRequestDirect(ctx context.Context, rawMsgID networkid.MessageID, key []byte) error { +func (wa *WhatsAppClient) sendMediaRequestDirect(rawMsgID networkid.MessageID, key []byte) error { msgID, err := waid.ParseMessageID(rawMsgID) if err != nil { return fmt.Errorf("failed to parse message ID: %w", err) } - return wa.Client.SendMediaRetryReceipt(ctx, &types.MessageInfo{ + return wa.Client.SendMediaRetryReceipt(&types.MessageInfo{ ID: msgID.ID, MessageSource: types.MessageSource{ IsFromMe: msgID.Sender.User == wa.JID.User, diff --git a/pkg/connector/proxy.go b/pkg/connector/proxy.go index 5ae103d..770d66d 100644 --- a/pkg/connector/proxy.go +++ b/pkg/connector/proxy.go @@ -58,12 +58,7 @@ func (wa *WhatsAppConnector) updateProxy(ctx context.Context, client *whatsmeow. } if proxy, err := wa.getProxy(reason); err != nil { return fmt.Errorf("failed to get proxy address: %w", err) - } else if proxy == "" { - return nil - } else if err = client.SetProxyAddress(proxy, whatsmeow.SetProxyOptions{ - OnlyLogin: wa.Config.ProxyOnlyLogin, - NoMedia: wa.Config.ProxyOnlyLogin, - }); err != nil { + } else if err = client.SetProxyAddress(proxy); err != nil { return fmt.Errorf("failed to set proxy address: %w", err) } zerolog.Ctx(ctx).Debug().Msg("Enabled proxy") diff --git a/pkg/connector/startchat.go b/pkg/connector/startchat.go index a0b0692..ee4ed9b 100644 --- a/pkg/connector/startchat.go +++ b/pkg/connector/startchat.go @@ -62,7 +62,7 @@ func looksEmaily(str string) bool { return false } -func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) (types.JID, error) { +func (wa *WhatsAppClient) validateIdentifer(number string) (types.JID, error) { if strings.HasSuffix(number, "@"+types.BotServer) || strings.HasSuffix(number, "@"+types.HiddenUserServer) { return types.ParseJID(number) } else if strings.HasPrefix(number, waid.BotPrefix) || strings.HasPrefix(number, waid.LIDPrefix) { @@ -76,7 +76,7 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) return types.EmptyJID, ErrInputLooksLikeEmail } else if wa.Client == nil || !wa.Client.IsLoggedIn() { return types.EmptyJID, bridgev2.ErrNotLoggedIn - } else if resp, err := wa.Client.IsOnWhatsApp(ctx, []string{number}); err != nil { + } else if resp, err := wa.Client.IsOnWhatsApp([]string{number}); err != nil { return types.EmptyJID, fmt.Errorf("failed to check if number is on WhatsApp: %w", err) } else if len(resp) == 0 { return types.EmptyJID, fmt.Errorf("the server did not respond to the query") @@ -144,7 +144,7 @@ func (wa *WhatsAppClient) CreateChatWithGhost(ctx context.Context, ghost *bridge } func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier string, startChat bool) (*bridgev2.ResolveIdentifierResponse, error) { - origJID, err := wa.validateIdentifer(ctx, identifier) + origJID, err := wa.validateIdentifer(identifier) if err != nil { return nil, err } @@ -189,7 +189,7 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl } resp := make([]*bridgev2.ResolveIdentifierResponse, 0, len(contacts)) for jid, contactInfo := range contacts { - if onlyContacts && (contactInfo.FirstName == "" && contactInfo.FullName == "") { + if onlyContacts && contactInfo.FirstName == "" { continue } if !matchesQuery(contactInfo.PushName, filter) && !matchesQuery(contactInfo.FullName, filter) && !matchesQuery(jid.User, filter) { @@ -217,13 +217,7 @@ func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.Grou CreateKey: createKey, } for i, participant := range params.Participants { - jid := waid.ParseUserID(participant) - // Normalize to PN if it's a LID - jid, err := wa.startChatLIDToPN(ctx, jid) - if err != nil { - return nil, fmt.Errorf("failed to normalize participant %s: %w", participant, err) - } - req.Participants[i] = jid + req.Participants[i] = waid.ParseUserID(participant) } if params.Parent != nil { var err error @@ -240,10 +234,10 @@ func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.Grou } var avatarBytes []byte var avatarMXC id.ContentURIString - if params.Avatar != nil && params.Avatar.URL != "" { + if params.Avatar != nil { avatarMXC = params.Avatar.URL var err error - avatarBytes, err = wa.Main.Bridge.Bot.DownloadMedia(ctx, params.Avatar.URL, nil) + avatarBytes, err = wa.Main.Bridge.Bot.DownloadMedia(ctx, params.Avatar.URL, params.Avatar.MSC3414File) if err != nil { return nil, fmt.Errorf("failed to download avatar: %w", err) } @@ -324,14 +318,13 @@ func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.Grou } changed := false if avatarBytes != nil { - avatarID, err := wa.Client.SetGroupPhoto(ctx, resp.JID, avatarBytes) + avatarID, err := wa.Client.SetGroupPhoto(resp.JID, avatarBytes) if err != nil { zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set group avatar after creating group") } else { portal.AvatarID = networkid.AvatarID(avatarID) portal.AvatarHash = sha256.Sum256(avatarBytes) portal.AvatarMXC = avatarMXC - portal.AvatarSet = true groupInfo.Avatar = &bridgev2.Avatar{ ID: portal.AvatarID, MXC: portal.AvatarMXC, @@ -342,7 +335,7 @@ func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.Grou } if params.Topic != nil { newTopicID := wa.Client.GenerateMessageID() - err = wa.Client.SetGroupTopic(ctx, resp.JID, "", newTopicID, params.Topic.Topic) + err = wa.Client.SetGroupTopic(resp.JID, "", newTopicID, params.Topic.Topic) if err != nil { zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set group topic after creating group") } else { diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index aa84938..150a97d 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -149,7 +149,7 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID return } log.Debug().Array("jids", exzerolog.ArrayOfStringers(ghostJIDs)).Msg("Doing background sync for users") - infos, err := wa.Client.GetUserInfo(ctx, ghostJIDs) + infos, err := wa.Client.GetUserInfo(ghostJIDs) if err != nil { log.Err(err).Msg("Failed to get user info for background sync") return @@ -194,30 +194,29 @@ func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, } else if jid == types.LegacyPSAJID || jid == types.PSAJID { contact.PushName = "WhatsApp" } - var altJID types.JID - if jid.Server == types.DefaultUserServer || jid.Server == types.HiddenUserServer { - var err error - altJID, err = wa.GetStore().GetAltJID(ctx, jid) + var phone string + if jid.Server == types.DefaultUserServer { + phone = "+" + jid.User + } else if jid.Server == types.HiddenUserServer { + pnJID, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid) if err != nil { - zerolog.Ctx(ctx).Err(err).Stringer("source_jid", jid).Msg("Failed to get alt JID") - } else if altJID.IsEmpty() { - zerolog.Ctx(ctx).Debug().Stringer("source_jid", jid).Msg("Alternate JID not found in contactToUserInfo") + zerolog.Ctx(ctx).Err(err).Stringer("lid", jid).Msg("Failed to get PN for LID") + } else if pnJID.IsEmpty() { + zerolog.Ctx(ctx).Debug().Stringer("lid", jid).Msg("Phone number not found for LID in contactToUserInfo") } else { - extraContact, err := wa.GetStore().Contacts.GetContact(ctx, altJID) + phone = "+" + pnJID.User + extraContact, err := wa.GetStore().Contacts.GetContact(ctx, pnJID) if err != nil { zerolog.Ctx(ctx).Err(err). - Stringer("source_jid", jid). - Stringer("alt_jid", altJID). - Msg("Failed to get contact info from alternate JID") + Stringer("lid", jid). + Stringer("pn_jid", pnJID). + Msg("Failed to get contact info from PN") } else { - // Phone contact info should only be stored for phone number JIDs - if altJID.Server == types.DefaultUserServer { - if contact.FirstName == "" { - contact.FirstName = extraContact.FirstName - } - if contact.FullName == "" { - contact.FullName = extraContact.FullName - } + if contact.FirstName == "" { + contact.FirstName = extraContact.FirstName + } + if contact.FullName == "" { + contact.FullName = extraContact.FullName } if contact.PushName == "" { contact.PushName = extraContact.PushName @@ -225,37 +224,9 @@ func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, if contact.BusinessName == "" { contact.BusinessName = extraContact.BusinessName } - if contact.PushName != "" && extraContact.PushName != "" && contact.PushName != extraContact.PushName { - zerolog.Ctx(ctx).Debug(). - Stringer("source_jid", jid). - Stringer("alt_jid", altJID). - Str("source_push_name", contact.PushName). - Str("alt_push_name", extraContact.PushName). - Msg("Conflicting push names between JIDs") - if altJID.Server == types.DefaultUserServer { - contact.PushName = extraContact.PushName - } - } - if contact.BusinessName != "" && extraContact.BusinessName != "" && contact.BusinessName != extraContact.BusinessName { - zerolog.Ctx(ctx).Debug(). - Stringer("source_jid", jid). - Stringer("alt_jid", altJID). - Str("source_push_name", contact.BusinessName). - Str("alt_push_name", extraContact.BusinessName). - Msg("Conflicting business names between JIDs") - if altJID.Server == types.DefaultUserServer { - contact.BusinessName = extraContact.BusinessName - } - } } } } - var phone string - if jid.Server == types.DefaultUserServer { - phone = "+" + jid.User - } else if altJID.Server == types.DefaultUserServer { - phone = "+" + altJID.User - } ui := &bridgev2.UserInfo{ Name: ptr.Ptr(wa.Main.Config.FormatDisplayname(jid, phone, contact)), IsBot: ptr.Ptr(jid.IsBot()), @@ -331,7 +302,7 @@ func (wa *WhatsAppClient) fetchGhostAvatar(ctx context.Context, ghost *bridgev2. existingID = "" } var wrappedAvatar *bridgev2.Avatar - avatar, err := wa.Client.GetProfilePictureInfo(ctx, jid, &whatsmeow.GetProfilePictureParams{ExistingID: existingID}) + avatar, err := wa.Client.GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{ExistingID: existingID}) if errors.Is(err, whatsmeow.ErrProfilePictureNotSet) { wrappedAvatar = &bridgev2.Avatar{ ID: "remove", diff --git a/pkg/connector/wadb/message.go b/pkg/connector/wadb/message.go index 4b16002..f2450c1 100644 --- a/pkg/connector/wadb/message.go +++ b/pkg/connector/wadb/message.go @@ -116,12 +116,9 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo AsList() } -func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) (int64, error) { - res, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after) - if err != nil { - return 0, err - } - return res.RowsAffected() +func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) error { + _, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after) + return err } 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 } -func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (int64, error) { - res, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID) - if err != nil { - return 0, err - } - return res.RowsAffected() +func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) error { + _, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID) + return err } func (mq *MessageQuery) ConversationHasMessages(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (exists bool, err error) { diff --git a/pkg/connector/wadb/upgrades/00-latest-schema.sql b/pkg/connector/wadb/upgrades/00-latest-schema.sql index 850f5b4..97275f5 100644 --- a/pkg/connector/wadb/upgrades/00-latest-schema.sql +++ b/pkg/connector/wadb/upgrades/00-latest-schema.sql @@ -1,4 +1,4 @@ --- v0 -> v9 (compatible with v3+): Latest revision +-- v0 -> v7 (compatible with v3+): Latest revision CREATE TABLE whatsapp_poll_option_id ( bridge_id TEXT NOT NULL, diff --git a/pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql b/pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql deleted file mode 100644 index d5b4ca0..0000000 --- a/pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql +++ /dev/null @@ -1,2 +0,0 @@ --- v8 (compatible with v3+): Mark LID DMs for deletion -INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false'); diff --git a/pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql b/pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql deleted file mode 100644 index 5c32e65..0000000 --- a/pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql +++ /dev/null @@ -1,3 +0,0 @@ --- v9 (compatible with v3+): Mark LID DMs for deletion (again) -DELETE FROM kv_store WHERE bridge_id='' AND key='whatsapp_lid_dms_deleted'; -INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false'); diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index 93de90c..78b8554 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -19,7 +19,6 @@ package msgconv import ( "bytes" "context" - "encoding/base64" "encoding/json" "errors" "fmt" @@ -50,13 +49,7 @@ import ( "go.mau.fi/mautrix-whatsapp/pkg/waid" ) -func (mc *MessageConverter) generateContextInfo( - ctx context.Context, - replyTo *database.Message, - portal *bridgev2.Portal, - perMessageTimer *event.BeeperDisappearingTimer, - roomMention bool, -) *waE2E.ContextInfo { +func (mc *MessageConverter) generateContextInfo(ctx context.Context, replyTo *database.Message, portal *bridgev2.Portal, perMessageTimer *event.BeeperDisappearingTimer) *waE2E.ContextInfo { contextInfo := &waE2E.ContextInfo{} if replyTo != nil { msgID, err := waid.ParseMessageID(replyTo.ID) @@ -64,7 +57,6 @@ func (mc *MessageConverter) generateContextInfo( contextInfo.StanzaID = proto.String(msgID.ID) contextInfo.Participant = proto.String(msgID.Sender.String()) contextInfo.QuotedMessage = &waE2E.Message{Conversation: proto.String("")} - contextInfo.QuotedType = waE2E.ContextInfo_EXPLICIT.Enum() } else { zerolog.Ctx(ctx).Warn().Err(err). Stringer("reply_to_event_id", replyTo.MXID). @@ -85,9 +77,6 @@ func (mc *MessageConverter) generateContextInfo( if setAt > 0 && contextInfo.Expiration != nil { contextInfo.EphemeralSettingTimestamp = ptr.Ptr(setAt) } - if roomMention { - contextInfo.NonJIDMentions = proto.Uint32(1) - } return contextInfo } @@ -107,7 +96,7 @@ func (mc *MessageConverter) ToWhatsApp( } message := &waE2E.Message{} - contextInfo := mc.generateContextInfo(ctx, replyTo, portal, content.BeeperDisappearingTimer, content.Mentions != nil && content.Mentions.Room) + contextInfo := mc.generateContextInfo(ctx, replyTo, portal, content.BeeperDisappearingTimer) switch content.MsgType { case event.MsgText, event.MsgNotice, event.MsgEmote: @@ -202,7 +191,6 @@ func (mc *MessageConverter) constructMediaMessage( FileSHA256: uploaded.FileSHA256, FileLength: proto.Uint64(uploaded.FileLength), URL: proto.String(uploaded.URL), - IsLottie: proto.Bool(mime == "application/was"), }, } case event.MsgAudio: @@ -484,17 +472,6 @@ func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) { 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( ctx context.Context, content *event.MessageEventContent, ) (*whatsmeow.UploadResponse, []byte, string, error) { @@ -503,25 +480,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp( if content.FileName != "" { fileName = content.FileName } - var data []byte - 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) - } + data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File) if err != nil { return nil, nil, "", fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err) } @@ -539,14 +498,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp( case event.MessageType(event.EventSticker.Type): isSticker = true mediaType = whatsmeow.MediaImage - if mime == "video/lottie+json" { - // 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" { + if mime != "image/webp" || content.Info.Width != content.Info.Height { var size int data, size, err = mc.convertToWebP(data) if err != nil { diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go index 11569e2..c4be4a4 100644 --- a/pkg/msgconv/from-whatsapp.go +++ b/pkg/msgconv/from-whatsapp.go @@ -140,9 +140,6 @@ func (mc *MessageConverter) ToMatrix( isBackfill bool, previouslyConvertedPart *bridgev2.ConvertedMessagePart, ) *bridgev2.ConvertedMessage { - if waMsg == nil { - waMsg = &waE2E.Message{} - } ctx = context.WithValue(ctx, contextKeyClient, client) ctx = context.WithValue(ctx, contextKeyIntent, intent) ctx = context.WithValue(ctx, contextKeyPortal, portal) @@ -237,9 +234,6 @@ func (mc *MessageConverter) ToMatrix( part.Extra["fi.mau.whatsapp.source_broadcast_list"] = info.Chat.String() } mc.addMentions(ctx, contextInfo.GetMentionedJID(), part.Content) - if contextInfo.GetNonJIDMentions() == 1 { - part.Content.Mentions.Room = true - } cm := &bridgev2.ConvertedMessage{ Parts: []*bridgev2.ConvertedMessagePart{part}, @@ -263,27 +257,6 @@ func (mc *MessageConverter) ToMatrix( if chat.IsEmpty() { chat, _ = waid.ParsePortalID(portal.ID) } - // We reroute all DMs to the phone number JID, so reroute reply participants too - pcp = rerouteMessageKey(ctx, chat, pcp, getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID) - if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && pcp.Server == types.HiddenUserServer { - pcpPN, _ := store.LIDs.GetPNForLID(ctx, pcp) - zerolog.Ctx(ctx).Debug(). - Stringer("orig_participant", pcp). - Stringer("rerouted_participant", pcpPN). - Msg("Rerouting reply target (PN recipient in LID DM)") - if !pcpPN.IsEmpty() { - pcp = pcpPN - } - } else if store != nil && chat.Server == types.GroupServer && pcp.Server == types.DefaultUserServer && getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID { - pcpLID, _ := store.LIDs.GetLIDForPN(ctx, pcp) - zerolog.Ctx(ctx).Debug(). - Stringer("orig_participant", pcp). - Stringer("rerouted_participant", pcpLID). - Msg("Rerouting reply target (PN recipient in LID group)") - if !pcpLID.IsEmpty() { - pcp = pcpLID - } - } cm.ReplyTo = &networkid.MessageOptionalPartID{ MessageID: waid.MakeMessageID(chat, pcp, contextInfo.GetStanzaID()), } diff --git a/pkg/msgconv/matrixpoll.go b/pkg/msgconv/matrixpoll.go index 0dc3213..22cc7af 100644 --- a/pkg/msgconv/matrixpoll.go +++ b/pkg/msgconv/matrixpoll.go @@ -71,7 +71,7 @@ func (mc *MessageConverter) PollStartToWhatsApp( if maxAnswers >= len(content.PollStart.Answers) || maxAnswers < 0 { maxAnswers = 0 } - contextInfo := mc.generateContextInfo(ctx, replyTo, portal, nil, content.Mentions != nil && content.Mentions.Room) + contextInfo := mc.generateContextInfo(ctx, replyTo, portal, nil) var question string question, contextInfo.MentionedJID = mc.msc1767ToWhatsApp(ctx, content.PollStart.Question, content.Mentions) if len(question) == 0 { diff --git a/pkg/msgconv/msgconv.go b/pkg/msgconv/msgconv.go index 185ee0f..e4109c8 100644 --- a/pkg/msgconv/msgconv.go +++ b/pkg/msgconv/msgconv.go @@ -17,9 +17,6 @@ package msgconv import ( - "sync" - - "go.mau.fi/whatsmeow/types" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/format" @@ -46,16 +43,12 @@ type MessageConverter struct { DisableViewOnce bool DirectMedia bool OldMediaSuffix string - - stickerPackCache map[string]*types.StickerPack - stickerPackCacheLock sync.Mutex } func New(br *bridgev2.Bridge) *MessageConverter { mc := &MessageConverter{ - Bridge: br, - MaxFileSize: 50 * 1024 * 1024, - stickerPackCache: make(map[string]*types.StickerPack), + Bridge: br, + MaxFileSize: 50 * 1024 * 1024, } mc.HTMLParser = &format.HTMLParser{ PillConverter: mc.convertPill, diff --git a/pkg/msgconv/wa-business.go b/pkg/msgconv/wa-business.go index da416d5..c3ed976 100644 --- a/pkg/msgconv/wa-business.go +++ b/pkg/msgconv/wa-business.go @@ -66,10 +66,10 @@ func (mc *MessageConverter) convertTemplateMessage(ctx context.Context, info *ty if addButtonText { description += "\nUse the WhatsApp app to click buttons" } - content = strings.TrimSpace(fmt.Sprintf("%s\n\n%s", content, description)) + content = fmt.Sprintf("%s\n\n%s", content, description) } if footer := tpl.GetHydratedFooterText(); footer != "" { - content = strings.TrimSpace(fmt.Sprintf("%s\n\n%s", content, footer)) + content = fmt.Sprintf("%s\n\n%s", content, footer) } var convertedTitle *bridgev2.ConvertedMessagePart @@ -239,7 +239,7 @@ func (mc *MessageConverter) postProcessBusinessMessage(content string, headerMed converted.Content.Body += content contentHTML := parseWAFormattingToHTML(content, true) if contentHTML != event.TextToHTML(content) || converted.Content.FormattedBody != "" { - converted.Content.Format = event.FormatHTML + converted.Content.EnsureHasHTML() if converted.Content.FormattedBody != "" { converted.Content.FormattedBody += "

" } diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go index c2a8624..647e98f 100644 --- a/pkg/msgconv/wa-media.go +++ b/pkg/msgconv/wa-media.go @@ -17,6 +17,8 @@ package msgconv import ( + "archive/zip" + "bytes" "context" "encoding/json" "errors" @@ -24,18 +26,21 @@ import ( "io" "net/http" "os" + "path/filepath" + "strconv" "strings" "github.com/rs/zerolog" "go.mau.fi/util/exmime" "go.mau.fi/util/exslices" + "go.mau.fi/util/lottie" + "go.mau.fi/util/random" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/types" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/id" "go.mau.fi/mautrix-whatsapp/pkg/waid" ) @@ -82,11 +87,11 @@ func (mc *MessageConverter) convertMediaMessage( MimeType: msg.GetMimetype(), } if mc.DirectMedia { + preparedMedia.FillFileName() if preparedMedia.Info.MimeType == "application/was" { preparedMedia.Info.MimeType = "video/lottie+json" preparedMedia.FileName = "sticker.json" } - preparedMedia.FillFileName() var err error portal := getPortal(ctx) idOverride := getEditTargetID(ctx) @@ -193,9 +198,7 @@ type PreparedMedia struct { } func (pm *PreparedMedia) FillFileName() *PreparedMedia { - if pm.Type == event.EventSticker { - pm.FileName = "" - } else if pm.FileName == "" { + if pm.FileName == "" { pm.FileName = strings.TrimPrefix(string(pm.MsgType), "m.") + exmime.ExtensionFromMimetype(pm.Info.MimeType) } return pm @@ -234,21 +237,6 @@ type MediaMessageWithDuration interface { GetSeconds() uint32 } -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 { extraInfo := map[string]any{} data := &PreparedMedia{ @@ -260,22 +248,6 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia { "info": extraInfo, }, } - if durationMsg, ok := rawMsg.(MediaMessageWithDuration); ok { - data.Info.Duration = int(durationMsg.GetSeconds() * 1000) - } - if dimensionMsg, ok := rawMsg.(MediaMessageWithDimensions); ok { - data.Info.Width = int(dimensionMsg.GetWidth()) - data.Info.Height = int(dimensionMsg.GetHeight()) - } - if captionMsg, ok := rawMsg.(MediaMessageWithCaption); ok && captionMsg.GetCaption() != "" { - data.Body = captionMsg.GetCaption() - } else { - data.Body = data.FileName - } - data.Info.Size = int(rawMsg.GetFileLength()) - data.Info.MimeType = rawMsg.GetMimetype() - data.ContextInfo = rawMsg.GetContextInfo() - switch msg := rawMsg.(type) { case *waE2E.ImageMessage: data.MsgType = event.MsgImage @@ -297,11 +269,12 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia { case *waE2E.StickerMessage: data.Type = event.EventSticker data.FileName = "sticker" + exmime.ExtensionFromMimetype(msg.GetMimetype()) - fixStickerDimensions(data.Info) + if msg.GetMimetype() == "application/was" && data.FileName == "sticker" { + data.FileName = "sticker.json" + } case *waE2E.VideoMessage: data.MsgType = event.MsgVideo - pairedMediaType := msg.GetContextInfo().GetPairedMediaType() - if msg.GetGifPlayback() || pairedMediaType == waE2E.ContextInfo_MOTION_PHOTO_PARENT || pairedMediaType == waE2E.ContextInfo_MOTION_PHOTO_CHILD { + if msg.GetGifPlayback() { extraInfo["fi.mau.gif"] = true extraInfo["fi.mau.loop"] = true extraInfo["fi.mau.autoplay"] = true @@ -312,7 +285,22 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia { default: panic(fmt.Errorf("unknown media message type %T", rawMsg)) } + if durationMsg, ok := rawMsg.(MediaMessageWithDuration); ok { + data.Info.Duration = int(durationMsg.GetSeconds() * 1000) + } + if dimensionMsg, ok := rawMsg.(MediaMessageWithDimensions); ok { + data.Info.Width = int(dimensionMsg.GetWidth()) + data.Info.Height = int(dimensionMsg.GetHeight()) + } + if captionMsg, ok := rawMsg.(MediaMessageWithCaption); ok && captionMsg.GetCaption() != "" { + data.Body = captionMsg.GetCaption() + } else { + data.Body = data.FileName + } + data.Info.Size = int(rawMsg.GetFileLength()) + data.Info.MimeType = rawMsg.GetMimetype() + data.ContextInfo = rawMsg.GetContextInfo() return data } @@ -357,15 +345,12 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment( ) error { client := getClient(ctx) intent := getIntent(ctx) - var roomID id.RoomID - if portal := getPortal(ctx); portal != nil { - roomID = portal.MXID - } + portal := getPortal(ctx) var thumbnailData []byte var thumbnailInfo *event.FileInfo if part.Info.Size > uploadFileThreshold { 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)) 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") @@ -398,14 +383,12 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment( if err != nil { return err } - } else if part.Type == event.EventSticker && part.Info.MimeType == "image/webp" { - mc.fillWebPStickerInfo(ctx, part, data) } if part.Info.MimeType == "" { part.Info.MimeType = http.DetectContentType(data) } 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 { return fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err) } @@ -414,7 +397,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment( var err error part.Info.ThumbnailURL, part.Info.ThumbnailFile, err = intent.UploadMedia( ctx, - roomID, + portal.MXID, thumbnailData, "thumbnail"+exmime.ExtensionFromMimetype(thumbnailInfo.MimeType), thumbnailInfo.MimeType, @@ -428,6 +411,68 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment( 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 { logLevel := zerolog.ErrorLevel var extra map[string]any @@ -472,3 +517,28 @@ func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *Pre } 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 +} diff --git a/pkg/msgconv/wa-misc.go b/pkg/msgconv/wa-misc.go index 7ca4427..8dcdfb9 100644 --- a/pkg/msgconv/wa-misc.go +++ b/pkg/msgconv/wa-misc.go @@ -27,7 +27,7 @@ import ( "github.com/rs/zerolog" "go.mau.fi/util/exerrors" "go.mau.fi/util/ptr" - "go.mau.fi/whatsmeow/proto/waAICommonDeprecated" + "go.mau.fi/whatsmeow/proto/waAICommon" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/types" "google.golang.org/protobuf/proto" @@ -266,9 +266,8 @@ func (mc *MessageConverter) convertKeepInChatMessage(ctx context.Context, msg *w func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { var body strings.Builder - // TODO switch to new format? for i, submsg := range msg.GetSubmessages() { - if submsg.GetMessageType() == waAICommonDeprecated.AIRichResponseSubMessageType_AI_RICH_RESPONSE_TEXT { + if submsg.GetMessageType() == waAICommon.AIRichResponseSubMessageType_AI_RICH_RESPONSE_TEXT { if i > 0 { body.WriteString("\n") } diff --git a/pkg/msgconv/wa-poll.go b/pkg/msgconv/wa-poll.go index 14388be..97a75e2 100644 --- a/pkg/msgconv/wa-poll.go +++ b/pkg/msgconv/wa-poll.go @@ -24,6 +24,7 @@ import ( "strings" "github.com/rs/zerolog" + "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waCommon" "go.mau.fi/whatsmeow/proto/waE2E" @@ -94,31 +95,7 @@ func (mc *MessageConverter) convertPollCreationMessage(ctx context.Context, msg }, msg.GetContextInfo() } -func rerouteMessageKey(ctx context.Context, chat, sender types.JID, groupLIDAddressing bool) types.JID { - if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && sender.Server == types.HiddenUserServer { - senderPN, _ := store.LIDs.GetPNForLID(ctx, sender) - zerolog.Ctx(ctx).Debug(). - Stringer("orig_participant", sender). - Stringer("rerouted_participant", senderPN). - Msg("Rerouting message key (PN recipient in LID DM)") - if !senderPN.IsEmpty() { - return senderPN - } - } else if store != nil && chat.Server == types.GroupServer && sender.Server == types.DefaultUserServer && groupLIDAddressing { - senderLID, _ := store.LIDs.GetLIDForPN(ctx, sender) - zerolog.Ctx(ctx).Debug(). - Stringer("orig_participant", sender). - Stringer("rerouted_participant", senderLID). - Msg("Rerouting message key (PN recipient in LID group)") - if !senderLID.IsEmpty() { - return senderLID - } - } - return sender -} - -func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender types.JID, key *waCommon.MessageKey) networkid.MessageID { - groupLIDAddressing := sender.Server == types.HiddenUserServer +func KeyToMessageID(client *whatsmeow.Client, chat, sender types.JID, key *waCommon.MessageKey) networkid.MessageID { sender = sender.ToNonAD() var err error if !key.GetFromMe() { @@ -132,21 +109,14 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender sender.Server = types.DefaultUserServer } } else if chat.Server == types.DefaultUserServer || chat.Server == types.BotServer { - if sender.User == client.Store.GetJID().User || sender.User == client.Store.GetLID().User { - // Message key is not from the sender, but message sender (containing key) is me, - // so message key sender is the other user in the DM + ownID := ptr.Val(client.Store.ID).ToNonAD() + if sender.User == ownID.User { sender = chat } else { - // Message key is not from the sender, but message sender (containing key) is not me, - // so message key sender is me - sender = client.Store.GetJID().ToNonAD() + sender = ownID } } else { - zerolog.Ctx(ctx).Warn(). - Stringer("chat", chat). - Stringer("sender", sender). - Any("key", key). - Msg("Failed to get message ID from key") + // TODO log somehow? return "" } } @@ -157,10 +127,6 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender chat = remoteJID } } - sender = rerouteMessageKey( - context.WithValue(ctx, contextKeyClient, client), - chat, sender, groupLIDAddressing, - ) return waid.MakeMessageID(chat, sender, key.GetID()) } @@ -172,14 +138,11 @@ var failedPollUpdatePart = &bridgev2.ConvertedMessagePart{ func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *types.MessageInfo, msg *waE2E.PollUpdateMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { log := zerolog.Ctx(ctx) - pollMessageID := KeyToMessageID(ctx, getClient(ctx), info.Chat, info.Sender, msg.PollCreationMessageKey) + pollMessageID := KeyToMessageID(getClient(ctx), info.Chat, info.Sender, msg.PollCreationMessageKey) pollMessage, err := mc.Bridge.DB.Message.GetPartByID(ctx, getPortal(ctx).Receiver, pollMessageID, "") if err != nil { log.Err(err).Msg("Failed to get poll update target message") return failedPollUpdatePart, nil - } else if pollMessage == nil { - log.Warn().Str("target_message_id", string(pollMessageID)).Msg("Poll update target message not found") - return failedPollUpdatePart, nil } vote, err := getClient(ctx).DecryptPollVote(ctx, &events.Message{ Info: *info, diff --git a/pkg/msgconv/wa-sticker.go b/pkg/msgconv/wa-sticker.go deleted file mode 100644 index b9797a6..0000000 --- a/pkg/msgconv/wa-sticker.go +++ /dev/null @@ -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 . - -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) -} diff --git a/pkg/waid/dbmeta.go b/pkg/waid/dbmeta.go index 2785105..5f47bdd 100644 --- a/pkg/waid/dbmeta.go +++ b/pkg/waid/dbmeta.go @@ -20,12 +20,10 @@ import ( "crypto/ecdh" "crypto/rand" "encoding/json" - "time" "go.mau.fi/util/exerrors" "go.mau.fi/util/jsontime" "go.mau.fi/util/random" - "go.mau.fi/whatsmeow/appstate" "go.mau.fi/whatsmeow/types" ) @@ -39,11 +37,7 @@ type UserLoginMetadata struct { APNSEncPrivKey []byte `json:"apns_enc_privkey,omitempty"` LoggedInAt jsontime.Unix `json:"logged_in_at,omitempty"` - AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"` - HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"` - - MData json.RawMessage `json:"mdata,omitempty"` } type PushKeys struct { diff --git a/pkg/waid/mediaid.go b/pkg/waid/mediaid.go index 093ece1..6a94de4 100644 --- a/pkg/waid/mediaid.go +++ b/pkg/waid/mediaid.go @@ -33,7 +33,6 @@ const ( mediaIDTypeMessage = 255 mediaIDTypeAvatar = 254 mediaIDTypeCommunityAvatar = 253 - mediaIDTypeStickerPackItem = 252 ) func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID) networkid.MediaID { @@ -83,28 +82,9 @@ type AvatarMediaInfo struct { 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 { Message *ParsedMessageID Avatar *AvatarMediaInfo - Sticker *StickerPackMediaInfo UserLogin networkid.UserLoginID } @@ -158,24 +138,6 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) { Community: mediaIDType == mediaIDTypeCommunityAvatar, } 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: 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) { var defVal T if len(*data) < 1 {