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 c9fef73..343443b 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -11,14 +11,14 @@ jobs:
strategy:
fail-fast: false
matrix:
- go-version: ["1.26", "1.27"]
- name: Lint ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }}
+ go-version: ["1.25", "1.26"]
+ name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@v6
- name: Set up Go
- uses: actions/setup-go@v7
+ uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d17865..3cefd6d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,34 +1,3 @@
-# v26.08
-
-* Switched direct chats to use LIDs instead of phone numbers.
-
-# v26.07
-
-* Updated Docker image to Alpine 3.24.
-* Added support for the extra passkey authentication step when logging in.
-* Added support for newer versions of poll messages from WhatsApp.
-* Fixed poll votes in DMs.
-
-# v26.06
-
-* Added placeholder for group message history share notices.
-* Fixed community spaces not being bridged properly in some cases.
-* Fixed handling edits of HD media.
-* Fixed duplicate message when a message from another own device is initially
- undecryptable and later gets resolved.
-
-# v26.05
-
-* Added support for importing sticker packs from WhatsApp.
-* Added support for WhatsApp's new message edit encryption scheme.
-
-# 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
diff --git a/Dockerfile b/Dockerfile
index e59d110..4efc9d5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1-alpine3.24 AS builder
+FROM golang:1-alpine3.23 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.24
+FROM alpine:3.23
ENV UID=1337 \
GID=1337
diff --git a/Dockerfile.ci b/Dockerfile.ci
index 0a73ff5..cb3be51 100644
--- a/Dockerfile.ci
+++ b/Dockerfile.ci
@@ -1,6 +1,6 @@
ARG DOCKER_HUB="docker.io"
-FROM ${DOCKER_HUB}/alpine:3.24
+FROM ${DOCKER_HUB}/alpine:3.23
ENV UID=1337 \
GID=1337
diff --git a/cmd/mautrix-whatsapp/legacyprovision.go b/cmd/mautrix-whatsapp/legacyprovision.go
new file mode 100644
index 0000000..f0527b2
--- /dev/null
+++ b/cmd/mautrix-whatsapp/legacyprovision.go
@@ -0,0 +1,153 @@
+package main
+
+import (
+ "net/http"
+ "strings"
+
+ "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"
+
+ "go.mau.fi/mautrix-whatsapp/pkg/connector"
+ "go.mau.fi/mautrix-whatsapp/pkg/waid"
+)
+
+type OtherUserInfo struct {
+ MXID id.UserID `json:"mxid"`
+ JID types.JID `json:"jid"`
+ Name string `json:"displayname"`
+ Avatar id.ContentURIString `json:"avatar_url"`
+}
+
+type PortalInfo struct {
+ RoomID id.RoomID `json:"room_id"`
+ OtherUser *OtherUserInfo `json:"other_user,omitempty"`
+ GroupInfo *types.GroupInfo `json:"group_info,omitempty"`
+ JustCreated bool `json:"just_created"`
+}
+
+type Error struct {
+ Success bool `json:"success"`
+ Error string `json:"error"`
+ ErrCode string `json:"errcode"`
+}
+
+func legacyProvContacts(w http.ResponseWriter, r *http.Request) {
+ userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
+ if userLogin == nil {
+ return
+ }
+ if contacts, err := userLogin.Client.(*connector.WhatsAppClient).GetStore().Contacts.GetAllContacts(r.Context()); err != nil {
+ hlog.FromRequest(r).Err(err).Msg("Failed to fetch all contacts")
+ exhttp.WriteJSONResponse(w, http.StatusInternalServerError, Error{
+ Error: "Internal server error while fetching contact list",
+ ErrCode: "failed to get contacts",
+ })
+ } else {
+ augmentedContacts := map[types.JID]any{}
+ for jid, contact := range contacts {
+ var avatarURL id.ContentURIString
+ if puppet, _ := m.Bridge.GetExistingGhostByID(r.Context(), waid.MakeUserID(jid)); puppet != nil {
+ avatarURL = puppet.AvatarMXC
+ }
+ augmentedContacts[jid] = map[string]interface{}{
+ "Found": contact.Found,
+ "FirstName": contact.FirstName,
+ "FullName": contact.FullName,
+ "PushName": contact.PushName,
+ "BusinessName": contact.BusinessName,
+ "AvatarURL": avatarURL,
+ }
+ }
+ exhttp.WriteJSONResponse(w, http.StatusOK, augmentedContacts)
+ }
+}
+
+func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) {
+ number := r.PathValue("number")
+ userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
+ if userLogin == nil {
+ return
+ }
+ startChat := strings.Contains(r.URL.Path, "/v1/pm/")
+ resp, err := userLogin.Client.(*connector.WhatsAppClient).ResolveIdentifier(r.Context(), number, startChat)
+ if err != nil {
+ hlog.FromRequest(r).Warn().Err(err).Str("identifier", number).Msg("Failed to resolve identifier")
+ matrix.RespondWithError(w, err, "Internal error resolving identifier")
+ return
+ }
+ var portal *bridgev2.Portal
+ if startChat {
+ portal, err = m.Bridge.GetPortalByKey(r.Context(), resp.Chat.PortalKey)
+ if err != nil {
+ hlog.FromRequest(r).Warn().Err(err).Stringer("portal_key", resp.Chat.PortalKey).Msg("Failed to get portal by key")
+ matrix.RespondWithError(w, err, "Internal error getting portal by key")
+ return
+ }
+ err = portal.CreateMatrixRoom(r.Context(), userLogin, nil)
+ if err != nil {
+ hlog.FromRequest(r).Warn().Err(err).Stringer("portal_key", resp.Chat.PortalKey).Msg("Failed to create matrix room for portal")
+ matrix.RespondWithError(w, err, "Internal error creating matrix room for portal")
+ return
+ }
+ } else {
+ portal, _ = m.Bridge.GetExistingPortalByKey(r.Context(), resp.Chat.PortalKey)
+ }
+ var roomID id.RoomID
+ if portal != nil {
+ roomID = portal.MXID
+ }
+ exhttp.WriteJSONResponse(w, http.StatusOK, PortalInfo{
+ RoomID: roomID,
+ OtherUser: &OtherUserInfo{
+ JID: waid.ParseUserID(resp.UserID),
+ MXID: resp.Ghost.Intent.GetMXID(),
+ Name: resp.Ghost.Name,
+ Avatar: resp.Ghost.AvatarMXC,
+ },
+ })
+}
+
+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 011e9b0..e64f02f 100644
--- a/cmd/mautrix-whatsapp/main.go
+++ b/cmd/mautrix-whatsapp/main.go
@@ -18,12 +18,21 @@ var m = mxmain.BridgeMain{
Name: "mautrix-whatsapp",
URL: "https://github.com/mautrix/whatsapp",
Description: "A Matrix-WhatsApp puppeting bridge.",
- Version: "26.08",
+ Version: "26.03",
SemCalVer: true,
Connector: &connector.WhatsAppConnector{},
}
func main() {
+ m.PostStart = func() {
+ if m.Matrix.Provisioning != nil {
+ 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)
m.Run()
}
diff --git a/cmd/mautrix-whatsapp/plugin.go b/cmd/mautrix-whatsapp/plugin.go
new file mode 100644
index 0000000..a1c9ca0
--- /dev/null
+++ b/cmd/mautrix-whatsapp/plugin.go
@@ -0,0 +1,24 @@
+//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 f7a6451..e8f8bb1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,53 +1,53 @@
module go.mau.fi/mautrix-whatsapp
-go 1.26.0
+go 1.25.0
-toolchain go1.27.0
+toolchain go1.26.1
tool go.mau.fi/util/cmd/maubuild
require (
- github.com/lib/pq v1.12.3
- github.com/rs/zerolog v1.35.1
- github.com/tidwall/gjson v1.19.0
- go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde
- go.mau.fi/webp v0.3.0
- go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629
- golang.org/x/image v0.45.0
- golang.org/x/net v0.58.0
- golang.org/x/sync v0.22.0
- google.golang.org/protobuf v1.36.12
+ github.com/lib/pq v1.11.2
+ github.com/rs/zerolog v1.34.0
+ go.mau.fi/util v0.9.7
+ go.mau.fi/webp v0.2.0
+ go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4
+ golang.org/x/image v0.37.0
+ golang.org/x/net v0.52.0
+ golang.org/x/sync v0.20.0
+ google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e
+ maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7
)
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/beeper/argo-go v1.1.2 // indirect
- github.com/coder/websocket v1.8.15 // indirect
- github.com/coreos/go-systemd/v22 v22.7.0 // indirect
+ github.com/coder/websocket v1.8.14 // indirect
+ github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mattn/go-sqlite3 v1.14.49 // indirect
- github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b // indirect
+ github.com/mattn/go-sqlite3 v1.14.34 // indirect
+ github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // 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/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // 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.5 // indirect
- go.mau.fi/libsignal v0.2.2 // indirect
+ github.com/yuin/goldmark v1.7.16 // indirect
+ go.mau.fi/libsignal v0.2.1 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
- golang.org/x/crypto v0.55.0 // indirect
- golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect
- golang.org/x/mod v0.40.0 // indirect
- golang.org/x/sys v0.47.0 // indirect
- golang.org/x/text v0.41.0 // indirect
+ golang.org/x/crypto v0.49.0 // indirect
+ golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
+ golang.org/x/mod v0.34.0 // indirect
+ golang.org/x/sys v0.42.0 // indirect
+ golang.org/x/text v0.35.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 1adbb52..6fedc40 100644
--- a/go.sum
+++ b/go.sum
@@ -8,15 +8,17 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
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.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
-github.com/coder/websocket v1.8.15/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/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.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
+github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/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=
@@ -28,17 +30,21 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.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.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
+github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/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.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w=
-github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
-github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b h1:sS7HLzwS+dO+gxATgQfeZDEdUZe2pKAB3nGoUwP5zU0=
-github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
+github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
+github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 h1:rh2lKw/P/EqHa724vYH2+VVQ1YnW4u6EOXl0PMAovZE=
+github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/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 +52,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/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=
@@ -55,8 +61,8 @@ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDq
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
-github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
+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/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
@@ -67,37 +73,39 @@ 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.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
-github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
-go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU=
-go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0=
-go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbsxj7G6mfujjKei1HaFQM=
-go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde/go.mod h1:z0ZZNt4hq3FZbUKnunexE/QscCx7VkLvQSvtggc/aE8=
-go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA=
-go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo=
-go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629 h1:99p9fTS1G73a1aifCVaV5MX4hDq0lVUKWGoUNoXyE3A=
-go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg=
+github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
+github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
+go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
+go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
+go.mau.fi/util v0.9.7 h1:AWGNbJfz1zRcQOKeOEYhKUG2fT+/26Gy6kyqcH8tnBg=
+go.mau.fi/util v0.9.7/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE=
+go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
+go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
+go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4 h1:FGA3NtCVNeCJ+C+KBg1pODsrfxC/trM3RHFWIeY7y4c=
+go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
-golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
-golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
-golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY=
-golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk=
-golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
-golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
-golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
-golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
-golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
-golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
-golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
-golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
+golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
+golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
+golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
+golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
+golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
+golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
+golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
+golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
+golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
-golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
-golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
-google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
-google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
+golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
+golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/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 +115,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.30.1-0.20260820142923-ae00f2c62e1e h1:ATvERyBIUXJrnL7ezP2xCLLGCNmGClmfzoQq4N3jBcA=
-maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0=
+maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7 h1:KUhlBHWGgknqYC2V8di4DFNh73atDtgPlqqO5FoLmPc=
+maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7/go.mod h1:YWw8NWTszsbyFAznboicBObwHPgTSLcuTbVX2kY7U2M=
diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go
index bb555e6..21d0c4d 100644
--- a/pkg/connector/backfill.go
+++ b/pkg/connector/backfill.go
@@ -12,7 +12,6 @@ import (
"time"
"github.com/rs/zerolog"
- "go.mau.fi/util/exmaps"
"go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waE2E"
@@ -116,30 +115,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, evt, blob, true)
if innerErr != nil {
return
}
@@ -151,16 +134,6 @@ 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
}
@@ -170,9 +143,9 @@ func (wa *WhatsAppClient) handleWAHistorySync(
notif *waE2E.HistorySyncNotification,
evt *waHistorySync.HistorySync,
stopOnError bool,
-) error {
+) (bool, error) {
if evt == nil || evt.SyncType == nil {
- return nil
+ return false, nil
}
log := wa.UserLogin.Log.With().
Str("action", "store history sync").
@@ -197,7 +170,7 @@ 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())).
@@ -205,8 +178,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
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())).
+ Any("access_status", notif.GetMessageAccessStatus())).
Msg("Storing history sync")
start := time.Now()
successfullySavedTotal := 0
@@ -229,32 +201,30 @@ func (wa *WhatsAppClient) handleWAHistorySync(
} else {
totalMessageCount += len(conv.GetMessages())
}
- if jid.Server == types.DefaultUserServer {
- lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid)
+ if jid.Server == types.HiddenUserServer {
+ pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid)
if err != nil {
- log.Err(err).Stringer("pn", jid).Msg("Failed to get LID for phone number in history sync")
- } else if lid.IsEmpty() {
- log.Warn().Stringer("pn", jid).Msg("No LID found for phone number in history sync")
+ 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", lid).
- Stringer("pn", jid).
- Msg("Rerouting phone number DM to LID in history sync")
- jid = lid
+ 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)
})
- var firstItemTime, lastItemTime time.Time
+ var minTime, maxTime time.Time
+ var minTimeIndex, maxTimeIndex int
ignoredTypes := 0
- rawMessages := conv.GetMessages()
- messages := make([]*wadb.HistorySyncMessageTuple, 0, len(rawMessages))
- allowClamp := conv.GetCommentsCount() == 0
- var newerTS uint64
- for i, rawMsg := range rawMessages {
+ messages := make([]*wadb.HistorySyncMessageTuple, 0, len(conv.GetMessages()))
+ for i, rawMsg := range conv.GetMessages() {
// Don't store messages that will just be skipped.
msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage())
if err != nil {
@@ -263,39 +233,22 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()).
Msg("Dropping historical message due to parse error")
- log.Trace().
- Any("web_message_info", rawMsg.GetMessage()).
- Msg("Content of event that failed to parse")
continue
}
- if firstItemTime.IsZero() {
- firstItemTime = msgEvt.Info.Timestamp
+ if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) {
+ minTime = msgEvt.Info.Timestamp
+ minTimeIndex = i
+ }
+ if maxTime.IsZero() || msgEvt.Info.Timestamp.After(maxTime) {
+ maxTime = msgEvt.Info.Timestamp
+ maxTimeIndex = i
}
- lastItemTime = msgEvt.Info.Timestamp
msgType := getMessageType(msgEvt.Message)
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
ignoredTypes++
continue
}
- // Comments (replies) in announcement groups are not ordered by timestamp, so don't clamp them.
- if rawMsg.GetMessage().GetCommentMetadata().GetCommentParentKey() != nil {
- allowClamp = false
- }
- // WhatsApp has bugs where some random messages will have timestamps decades in the future.
- // To ensure they don't mess up our ordering, require timestamps of older messages to be
- // before the previous (newer) message.
- if currentTS := rawMsg.GetMessage().GetMessageTimestamp(); newerTS > 0 && allowClamp && currentTS > newerTS {
- log.Warn().
- Time("current_ts", time.Unix(int64(currentTS), 0)).
- Time("prev_ts", time.Unix(int64(newerTS), 0)).
- Int("msg_index", i).
- Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
- Msg("Clamping message timestamp")
- rawMsg.Message.MessageTimestamp = ptr.Ptr(newerTS)
- msgEvt.Info.Timestamp = time.Unix(int64(newerTS), 0)
- }
- newerTS = rawMsg.GetMessage().GetMessageTimestamp()
marshaled, err := proto.Marshal(rawMsg)
if err != nil {
log.Warn().Err(err).
@@ -310,8 +263,10 @@ func (wa *WhatsAppClient) handleWAHistorySync(
log.Debug().
Int("wrapped_count", len(messages)).
Int("ignored_msg_type_count", ignoredTypes).
- Time("first_item_time", firstItemTime).
- Time("last_item_time", lastItemTime).
+ Time("lowest_time", minTime).
+ Int("lowest_time_index", minTimeIndex).
+ Time("highest_time", maxTime).
+ Int("highest_time_index", maxTimeIndex).
Dict("metadata", zerolog.Dict().
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
@@ -320,17 +275,15 @@ 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")
if len(messages) > 0 {
- err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, firstItemTime))
+ err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime))
if err != nil {
if stopOnError {
- return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
+ return false, fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
}
log.Err(err).Msg("Failed to save conversation metadata")
continue
@@ -338,7 +291,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)
@@ -348,7 +301,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")
}
@@ -360,7 +313,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) {
@@ -408,8 +363,8 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
return
}
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true)
- if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) {
- log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID).
+ if errors.Is(err, whatsmeow.ErrNotInGroup) {
+ log.Debug().Stringer("chat_jid", conv.ChatJID).
Msg("Skipping creating room because the user is not a participant")
//err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID)
//if err != nil {
@@ -482,39 +437,26 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
if err != nil {
return nil, err
}
- if portalJID.Server == types.DefaultUserServer {
- zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("FetchMessages called for phone number portal")
- }
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)
- }
- 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
- }
+ } 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)
}
+ endTime = ptr.Ptr(time.Unix(endTimeUnix, 0))
+ } else if params.AnchorMessage != nil {
+ endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
}
var anchorID types.MessageID
if params.AnchorMessage != nil {
@@ -523,31 +465,19 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
anchorID = parsedID.ID
}
}
- var hasMore bool
- if !params.Forward && wa.Main.Config.HistorySync.BackwardsOnDemand {
- hasMore = conv != nil && ptr.Val(conv.EndOfHistoryTransferType) == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
- }
messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1)
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, portalJID, params)
- }
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.
@@ -558,90 +488,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
- dups := make(exmaps.Set[networkid.MessageID])
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")
- zerolog.Ctx(ctx).Trace().
- Any("web_message_info", msg.GetMessage()).
- Msg("Content of event that failed to parse")
- continue
- }
- if !explodeOnError {
- msgType := getMessageType(evt.Message)
- if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
- continue
- }
- }
- if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, false) {
- return nil, fmt.Errorf("failed to ensure alt JIDs for message %s", evt.Info.ID)
+ // 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,
)
- // This is a hack to remove duplicates where the same message is inserted with both the LID and phone number sender
- // TODO prevent those being inserted in the first place instead of hacking around it here
- if !dups.Add(converted.ID) {
- continue
- }
- convertedMessages = append(convertedMessages, converted)
if mediaReq != nil {
mediaRequests = append(mediaRequests, mediaReq)
}
@@ -650,10 +509,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 {
@@ -671,109 +544,6 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
}, nil
}
-func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID types.JID, 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: portalJID,
- Sender: parsed.Sender,
- IsFromMe: wa.IsOwnJID(parsed.Sender),
- IsGroup: parsed.Chat.Server == types.GroupServer,
- },
- ID: parsed.ID,
- Timestamp: params.AnchorMessage.Timestamp,
- }, 50)
- zerolog.Ctx(ctx).Debug().
- Str("request_msg_id", msgID).
- Stringer("portal_jid", portalJID).
- 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
- }
- if portalJID.Server == types.DefaultUserServer {
- lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, portalJID)
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Stringer("lid", portalJID).Msg("Failed to get LID for phone number in on-demand history sync")
- } else if lid.IsEmpty() {
- zerolog.Ctx(ctx).Warn().Stringer("lid", portalJID).Msg("No LID found for phone number in on-demand history sync")
- } else {
- zerolog.Ctx(ctx).Debug().
- Stringer("lid", lid).
- Stringer("pn", portalJID).
- Msg("Rerouting phone number DM to LID in on-demand history sync")
- portalJID = lid
- }
- }
- 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) {
@@ -784,12 +554,11 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
}
// TODO use proper intent
intent := wa.Main.Bridge.Bot
- msgID := waid.MakeMessageIDWithAltSender(info.Chat, info.Sender, info.SenderAlt, info.ID)
wrapped := &bridgev2.BackfillMessage{
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil),
- Sender: wa.makeEventSender(ctx, pickLID(info.Sender, info.SenderAlt)),
- ID: msgID,
- TxnID: networkid.TransactionID(msgID),
+ Sender: wa.makeEventSender(ctx, info.Sender),
+ ID: waid.MakeMessageID(info.Chat, info.Sender, info.ID),
+ 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)),
@@ -798,10 +567,10 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
for _, reaction := range reactions {
var sender types.JID
if reaction.GetKey().GetFromMe() {
- sender = wa.GetLID()
+ sender = wa.JID
} else if reaction.GetKey().GetParticipant() != "" {
sender, _ = types.ParseJID(*reaction.Key.Participant)
- } else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BotServer {
+ } else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.BotServer {
sender = info.Chat
}
if sender.IsEmpty() {
diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go
index 152e7ad..202569b 100644
--- a/pkg/connector/capabilities.go
+++ b/pkg/connector/capabilities.go
@@ -19,7 +19,6 @@ var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true,
ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{
- ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true,
LookupPhone: true,
@@ -52,7 +51,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
}
func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) {
- return 1, 9
+ return 1, 7
}
const WAMaxFileSize = 2000 * 1024 * 1024
@@ -67,7 +66,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
}
func capID() string {
- base := "fi.mau.whatsapp.capabilities.2026_07_22"
+ base := "fi.mau.whatsapp.capabilities.2025_12_15"
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,
@@ -174,26 +173,21 @@ var whatsappCaps = &event.RoomFeatures{
event.MemberActionKick: event.CapLevelFullySupported,
event.MemberActionLeave: event.CapLevelFullySupported,
},
- MaxTextLength: MaxTextLength,
- LocationMessage: event.CapLevelFullySupported,
- Poll: event.CapLevelFullySupported,
- PollEnd: event.CapLevelUnsupported,
- PollHiddenVotes: event.CapLevelUnsupported,
- PollDuplicateOptions: event.CapLevelUnsupported,
- PollMaxOptions: 12,
- PollOptionMaxLength: 100,
- Reply: event.CapLevelFullySupported,
- Edit: event.CapLevelFullySupported,
- EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)),
- Delete: event.CapLevelFullySupported,
- DeleteForMe: false,
- DeleteMaxAge: ptr.Ptr(jsontime.S(2 * 24 * time.Hour)),
- Reaction: event.CapLevelFullySupported,
- ReactionCount: 1,
- ReadReceipts: true,
- TypingNotifications: true,
- DisappearingTimer: waDisappearingCap,
- DeleteChat: true,
+ MaxTextLength: MaxTextLength,
+ LocationMessage: event.CapLevelFullySupported,
+ Poll: event.CapLevelFullySupported,
+ Reply: event.CapLevelFullySupported,
+ Edit: event.CapLevelFullySupported,
+ EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)),
+ Delete: event.CapLevelFullySupported,
+ DeleteForMe: false,
+ DeleteMaxAge: ptr.Ptr(jsontime.S(2 * 24 * time.Hour)),
+ Reaction: event.CapLevelFullySupported,
+ ReactionCount: 1,
+ ReadReceipts: true,
+ TypingNotifications: true,
+ DisappearingTimer: waDisappearingCap,
+ DeleteChat: true,
}
var whatsappDMCaps *event.RoomFeatures
diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go
index 1461561..5555566 100644
--- a/pkg/connector/chatinfo.go
+++ b/pkg/connector/chatinfo.go
@@ -99,18 +99,6 @@ func (wa *WhatsAppClient) applyChatSettings(ctx context.Context, chatID types.JI
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings")
return
}
- if !chat.Found {
- chatID, err = wa.GetStore().GetAltJID(ctx, chatID)
- if err != nil {
- zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get alternate JID to get chat settings")
- return
- }
- chat, err = wa.GetStore().ChatSettings.GetChatSettings(ctx, chatID)
- if err != nil {
- zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings with alternate JID")
- return
- }
- }
info.UserLocal = &bridgev2.UserLocalPortalInfo{
MutedUntil: ptr.Ptr(chat.MutedUntil),
}
@@ -153,20 +141,15 @@ const PrivateChatTopic = "WhatsApp private chat"
const BotChatTopic = "WhatsApp chat with a bot"
func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridgev2.ChatInfo {
- ownID := wa.JID
- if jid.Server == types.HiddenUserServer {
- ownID = wa.GetLID()
- }
info := &bridgev2.ChatInfo{
- Type: ptr.Ptr(database.RoomTypeDM),
Topic: ptr.Ptr(PrivateChatTopic),
Members: &bridgev2.ChatMemberList{
IsFull: true,
TotalMemberCount: 2,
OtherUserID: waid.MakeUserID(jid),
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
- waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)},
- waid.MakeUserID(ownID): {EventSender: wa.makeEventSender(ctx, ownID)},
+ waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)},
+ waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)},
},
PowerLevels: &bridgev2.PowerLevelOverrides{
Events: map[event.Type]int{
@@ -176,14 +159,13 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
event.StateBeeperDisappearingTimer: 0,
},
},
- ExcludeChangesFromTimeline: true,
},
- ExcludeChangesFromTimeline: true,
+ Type: ptr.Ptr(database.RoomTypeDM),
}
if jid.Server == types.BotServer {
info.Topic = ptr.Ptr(BotChatTopic)
}
- if wa.IsOwnJID(jid) {
+ if jid == wa.JID.ToNonAD() {
// For chats with self, force-split the members so the user's own ghost is always in the room.
info.Members.MemberMap = map[networkid.UserID]bridgev2.ChatMember{
waid.MakeUserID(jid): {EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(jid)}},
@@ -207,7 +189,7 @@ func (wa *WhatsAppClient) wrapStatusBroadcastInfo(ctx context.Context) *bridgev2
Members: &bridgev2.ChatMemberList{
IsFull: false,
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
- waid.MakeUserID(wa.GetLID()): {EventSender: wa.makeEventSender(ctx, wa.GetLID())},
+ waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)},
},
},
Type: ptr.Ptr(database.RoomTypeDefault),
@@ -275,7 +257,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
Name: ptr.Ptr(info.Name),
Topic: ptr.Ptr(info.Topic),
Members: &bridgev2.ChatMemberList{
- IsFull: !info.IsIncognito && !info.IsParent,
+ IsFull: !info.IsIncognito,
TotalMemberCount: len(info.Participants),
MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)),
PowerLevels: &bridgev2.PowerLevelOverrides{
@@ -284,13 +266,14 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
Ban: ptr.Ptr(nobodyPL),
// TODO allow invites if bridge config says to allow them, or maybe if relay mode is enabled?
Events: map[event.Type]int{
- event.StateRoomName: metaChangePL,
- event.StateRoomAvatar: metaChangePL,
- event.StateTopic: metaChangePL,
- event.EventReaction: defaultPL,
- event.EventRedaction: defaultPL,
- event.EventUnstablePollResponse: defaultPL,
+ event.StateRoomName: metaChangePL,
+ event.StateRoomAvatar: metaChangePL,
+ event.StateTopic: metaChangePL,
+ event.EventReaction: defaultPL,
+ event.EventRedaction: defaultPL,
+
event.StateBeeperDisappearingTimer: metaChangePL,
+ // TODO always allow poll responses
},
},
},
@@ -301,15 +284,11 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
},
ExtraUpdates: extraUpdater,
}
- var hasSelf bool
for _, pcp := range info.Participants {
member := bridgev2.ChatMember{
EventSender: wa.makeEventSender(ctx, pcp.JID),
Membership: event.MembershipJoin,
}
- if member.EventSender.IsFromMe {
- hasSelf = true
- }
if pcp.IsSuperAdmin {
member.PowerLevel = ptr.Ptr(superAdminPL)
} else if pcp.IsAdmin {
@@ -320,21 +299,18 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
member.MemberEventExtra = map[string]any{
"com.beeper.exclude_from_timeline": true,
}
- wrapped.Members.MemberMap.Set(member)
+ wrapped.Members.MemberMap[waid.MakeUserID(pcp.JID)] = member
if pcp.JID.Server == types.HiddenUserServer && !pcp.PhoneNumber.IsEmpty() {
- wrapped.Members.MemberMap.Add(bridgev2.ChatMember{
+ wrapped.Members.MemberMap[waid.MakeUserID(pcp.PhoneNumber)] = bridgev2.ChatMember{
EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(pcp.PhoneNumber)},
Membership: event.MembershipLeave,
PrevMembership: event.MembershipJoin,
MemberEventExtra: map[string]any{
"com.beeper.exclude_from_timeline": true,
},
- })
+ }
}
}
- if info.IsParent && !hasSelf && info.AddressingMode == types.AddressingModeLID {
- wrapped.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: wa.makeEventSender(ctx, wa.GetLID())})
- }
if !info.LinkedParentJID.IsEmpty() {
wrapped.ParentID = ptr.Ptr(waid.MakePortalID(info.LinkedParentJID))
@@ -474,7 +450,7 @@ func (wa *WhatsAppClient) makePortalAvatarFetcher(avatarID string, sender types.
wrappedAvatar = &bridgev2.Avatar{
ID: networkid.AvatarID(avatar.ID),
Get: func(ctx context.Context) ([]byte, error) {
- return wa.Client.DownloadMediaWithOnlyPath(ctx, avatar.DirectPath)
+ return wa.Client.DownloadMediaWithPath(ctx, avatar.DirectPath, nil, nil, nil, 0, "", "")
},
}
}
@@ -513,7 +489,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
if info.ThreadMeta.Picture != nil {
avatar.ID = networkid.AvatarID(info.ThreadMeta.Picture.ID)
avatar.Get = func(ctx context.Context) ([]byte, error) {
- return wa.Client.DownloadMediaWithOnlyPath(ctx, info.ThreadMeta.Picture.DirectPath)
+ return wa.Client.DownloadMediaWithPath(ctx, info.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
}
} else if info.ThreadMeta.Preview.ID != "" {
avatar.ID = networkid.AvatarID(info.ThreadMeta.Preview.ID)
@@ -524,7 +500,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
} else if meta.ThreadMeta.Picture == nil {
return nil, fmt.Errorf("full res avatar info is missing")
}
- return wa.Client.DownloadMediaWithOnlyPath(ctx, meta.ThreadMeta.Picture.DirectPath)
+ return wa.Client.DownloadMediaWithPath(ctx, meta.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
}
} else {
avatar.ID = "remove"
@@ -540,8 +516,8 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
Members: &bridgev2.ChatMemberList{
TotalMemberCount: info.ThreadMeta.SubscriberCount,
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
- waid.MakeUserID(wa.GetLID()): {
- EventSender: wa.makeEventSender(ctx, wa.GetLID()),
+ waid.MakeUserID(wa.JID): {
+ EventSender: wa.makeEventSender(ctx, wa.JID),
PowerLevel: &ownPowerLevel,
},
},
@@ -550,12 +526,12 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
StateDefault: ptr.Ptr(nobodyPL),
Ban: ptr.Ptr(nobodyPL),
Events: map[event.Type]int{
- event.StateRoomName: adminPL,
- event.StateRoomAvatar: adminPL,
- event.StateTopic: adminPL,
- event.EventReaction: defaultPL,
- event.EventRedaction: defaultPL,
- event.EventUnstablePollResponse: defaultPL,
+ event.StateRoomName: adminPL,
+ event.StateRoomAvatar: adminPL,
+ event.StateTopic: adminPL,
+ event.EventReaction: defaultPL,
+ event.EventRedaction: defaultPL,
+ // TODO always allow poll responses
},
},
},
diff --git a/pkg/connector/client.go b/pkg/connector/client.go
index b3ecdba..367ad14 100644
--- a/pkg/connector/client.go
+++ b/pkg/connector/client.go
@@ -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"
)
@@ -54,9 +53,8 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
directMediaRetries: make(map[networkid.MessageID]*directMediaRetry),
mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle),
pushNamesSynced: exsync.NewEvent(),
+ createDedup: exsync.NewSet[types.MessageID](),
appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time),
-
- disableNewsletter: store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS,
}
login.Client = w
@@ -71,7 +69,6 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
if err != nil {
return err
}
- w.LID = w.Device.GetLID()
if w.Device != nil {
log := w.UserLogin.Log.With().Str("component", "whatsmeow").Logger()
@@ -106,7 +103,6 @@ type WhatsAppClient struct {
Client *whatsmeow.Client
Device *store.Device
JID types.JID
- LID types.JID
MC mClient
historySyncWakeup chan struct{}
@@ -121,8 +117,7 @@ type WhatsAppClient struct {
isNewLogin bool
pushNamesSynced *exsync.Event
lastPresence types.Presence
-
- disableNewsletter bool
+ createDedup *exsync.Set[types.MessageID]
appStateRecoveryLock sync.Mutex
appStateFullSyncAttempted map[appstate.WAPatchName]time.Time
@@ -133,7 +128,6 @@ var (
_ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil)
- _ bridgev2.StickerImportingNetworkAPI = (*WhatsAppClient)(nil)
)
var pushCfg = &bridgev2.PushConfig{
@@ -189,19 +183,7 @@ func (wa *WhatsAppClient) RegisterPushNotifications(ctx context.Context, pushTyp
}
func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) bool {
- return userID == waid.MakeUserID(wa.JID) || userID == waid.MakeUserID(wa.GetLID())
-}
-
-func (wa *WhatsAppClient) IsOwnJID(jid types.JID) bool {
- return (jid.Server == types.DefaultUserServer && jid.User == wa.JID.User) ||
- (jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User)
-}
-
-func (wa *WhatsAppClient) GetLID() types.JID {
- if wa.LID.IsEmpty() && !wa.JID.IsEmpty() {
- wa.LID = wa.GetStore().GetLID()
- }
- return wa.LID
+ return userID == waid.MakeUserID(wa.JID)
}
func (wa *WhatsAppClient) Connect(ctx context.Context) {
@@ -485,12 +467,3 @@ func (wa *WhatsAppClient) updatePresence(ctx context.Context, presence types.Pre
}
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/config.go b/pkg/connector/config.go
index e51c80a..6a097c2 100644
--- a/pkg/connector/config.go
+++ b/pkg/connector/config.go
@@ -2,7 +2,6 @@ package connector
import (
_ "embed"
- "fmt"
"strings"
"text/template"
"time"
@@ -71,8 +70,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:"-"`
@@ -91,15 +88,7 @@ func (c *Config) UnmarshalYAML(node *yaml.Node) error {
func (c *Config) PostProcess() error {
var err error
c.displaynameTemplate, err = template.New("displayname").Parse(c.DisplaynameTemplate)
- if err != nil {
- return err
- }
- // Try to execute template to make sure it's valid
- _, err = c.formatDisplayname(types.PSAJID, "", types.ContactInfo{})
- if err != nil {
- return fmt.Errorf("failed to execute displayname template: %w", err)
- }
- return nil
+ return err
}
func upgradeConfig(helper up.Helper) {
@@ -145,7 +134,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 {
@@ -160,7 +148,7 @@ type DisplaynameParams struct {
Short string
}
-func (c *Config) formatDisplayname(jid types.JID, phone string, contact types.ContactInfo) (string, error) {
+func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.ContactInfo) string {
var nameBuf strings.Builder
if phone == "" && jid.Server == types.DefaultUserServer {
phone = "+" + jid.User
@@ -179,21 +167,13 @@ func (c *Config) formatDisplayname(jid types.JID, phone string, contact types.Co
Name: contact.FullName,
Short: contact.FirstName,
})
- return nameBuf.String(), err
-}
-
-func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.ContactInfo) string {
- name, err := c.formatDisplayname(jid, phone, contact)
if err != nil {
panic(err)
}
- return name
+ return nameBuf.String()
}
func redactPhone(phone string) string {
- if len(phone) <= 4 {
- return phone
- }
// This doesn't keep 2+ digit country codes properly, but whatever
return phone[:2] + strings.Repeat("∙", len(phone)-4) + phone[len(phone)-2:]
}
diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go
index e262c82..91b1e11 100644
--- a/pkg/connector/connector.go
+++ b/pkg/connector/connector.go
@@ -28,24 +28,27 @@ import (
"time"
"github.com/lib/pq"
+ "github.com/rs/zerolog"
"go.mau.fi/util/dbutil"
- "go.mau.fi/util/exsync"
"go.mau.fi/util/random"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waCompanionReg"
"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 {
@@ -61,8 +64,6 @@ type WhatsAppConnector struct {
mediaEditCache MediaEditCache
mediaEditCacheLock sync.RWMutex
stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc]
-
- unmigratedDMs *exsync.Set[networkid.PortalKey]
}
func init() {
@@ -113,7 +114,6 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) {
cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin,
)
wa.mediaEditCache = make(MediaEditCache)
- wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]()
whatsmeowDBLog := bridge.Log.With().Str("db_section", "whatsmeow").Logger()
wa.DeviceStore = sqlstore.NewWithWrappedDB(
@@ -157,7 +157,78 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error {
return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"}
}
- return wa.migrateToLIDDMs(ctx)
+ 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() {
diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go
index 8a7afd9..a4a3d73 100644
--- a/pkg/connector/directmedia.go
+++ b/pkg/connector/directmedia.go
@@ -17,7 +17,6 @@
package connector
import (
- "bytes"
"context"
"encoding/json"
"errors"
@@ -52,7 +51,7 @@ func (wa *WhatsAppConnector) SetUseDirectMedia() {
}
var ErrReloadNeeded = mautrix.RespError{
- ErrCode: "COM.BEEPER.MEDIA_RELOAD_NEEDED",
+ ErrCode: "FI.MAU.WHATSAPP_RELOAD_NEEDED",
Err: "Media is no longer available on WhatsApp servers and must be re-requested from your phone",
StatusCode: http.StatusNotFound,
}
@@ -68,8 +67,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")
}
@@ -131,30 +128,15 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars
}
return &mediaproxy.GetMediaResponseFile{
Callback: func(w *os.File) (*mediaproxy.FileMeta, error) {
- return &mediaproxy.FileMeta{}, waClient.Client.DownloadMediaWithOnlyPathToFile(ctx, cachedInfo.DirectPath, w)
+ return &mediaproxy.FileMeta{}, waClient.Client.DownloadMediaWithPathToFile(
+ ctx, cachedInfo.DirectPath, nil, nil, nil, 0, "", "", w,
+ )
},
}, 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)
@@ -170,9 +152,6 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
if err != nil {
return nil, fmt.Errorf("failed to unmarshal media keys: %w", err)
}
- if version := parsedID.Message.Version; len(version) > 0 && !bytes.Equal(version, keys.EncSHA256) && !bytes.Equal(version, keys.SHA256) {
- return nil, mautrix.MNotFound.WithMessage("Version mismatch, media was likely replaced")
- }
var ul *bridgev2.UserLogin
if parsedID.UserLogin != "" {
ul = wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin)
@@ -195,29 +174,16 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
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)) {
- val := params["com.beeper.interactive_download_request"]
+ err := waClient.Client.DownloadToFile(ctx, keys, f)
+ if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent) {
+ val := params["fi.mau.whatsapp.reload_media"]
if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") {
return nil, 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
@@ -225,29 +191,30 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse(
log.Trace().Msg("Retrying download after successful retry")
err = waClient.Client.DownloadToFile(ctx, keys, f)
}
- if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
+ if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
} else if err != nil {
return nil, err
}
- if mimeType == "application/was" {
+ mime := keys.MimeType
+ if mime == "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)
} 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 {
+ } else if data, err := msgconv.ExtractAnimatedSticker(zipData); err != nil {
return nil, 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)
} else if err := f.Truncate(int64(len(data))); err != nil {
return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err)
}
- mimeType = "video/lottie+json"
+ mime = "video/lottie+json"
}
return &mediaproxy.FileMeta{
- ContentType: mimeType,
+ ContentType: mime,
}, nil
},
}, nil
diff --git a/pkg/connector/events.go b/pkg/connector/events.go
index 1f4e118..de5bdb8 100644
--- a/pkg/connector/events.go
+++ b/pkg/connector/events.go
@@ -39,6 +39,18 @@ import (
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)
+func (wa *WhatsAppClient) getPortalKeyByMessageSource(ms types.MessageSource) networkid.PortalKey {
+ jid := ms.Chat
+ if ms.IsIncomingBroadcast() {
+ if ms.IsFromMe {
+ jid = ms.BroadcastListOwner.ToNonAD()
+ } else {
+ jid = ms.Sender.ToNonAD()
+ }
+ }
+ return wa.makeWAPortalKey(jid)
+}
+
type MessageInfoWrapper struct {
Info types.MessageInfo
wa *WhatsAppClient
@@ -49,26 +61,7 @@ func (evt *MessageInfoWrapper) ShouldCreatePortal() bool {
}
func (evt *MessageInfoWrapper) GetPortalKey() networkid.PortalKey {
- ms := evt.Info.MessageSource
- jid := ms.Chat
- if ms.IsIncomingBroadcast() {
- if ms.IsFromMe {
- // TODO can this still be a phone number?
- jid = ms.BroadcastListOwner.ToNonAD()
- } else {
- jid = ms.Sender.ToNonAD()
- if jid.Server == types.DefaultUserServer && !ms.SenderAlt.IsEmpty() {
- jid = ms.SenderAlt.ToNonAD()
- }
- }
- } else if jid.Server == types.DefaultUserServer {
- if !ms.IsFromMe && ms.Chat.ToNonAD() == ms.Sender.ToNonAD() && !ms.SenderAlt.IsEmpty() {
- jid = ms.SenderAlt.ToNonAD()
- } else if !ms.RecipientAlt.IsEmpty() {
- jid = ms.RecipientAlt.ToNonAD()
- }
- }
- return evt.wa.makeWAPortalKey(jid)
+ return evt.wa.getPortalKeyByMessageSource(evt.Info.MessageSource)
}
func (evt *MessageInfoWrapper) AddLogContext(c zerolog.Context) zerolog.Context {
@@ -79,19 +72,12 @@ func (evt *MessageInfoWrapper) GetTimestamp() time.Time {
return evt.Info.Timestamp
}
-func pickLID(main, alt types.JID) types.JID {
- if main.Server == types.DefaultUserServer && alt.Server == types.HiddenUserServer {
- return alt
- }
- return main
-}
-
func (evt *MessageInfoWrapper) GetSender() bridgev2.EventSender {
- return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, pickLID(evt.Info.Sender, evt.Info.SenderAlt))
+ return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.Info.Sender)
}
func (evt *MessageInfoWrapper) GetID() networkid.MessageID {
- return waid.MakeMessageIDWithAltSender(evt.Info.Chat, evt.Info.Sender, evt.Info.SenderAlt, evt.Info.ID)
+ return waid.MakeMessageID(evt.Info.Chat, evt.Info.Sender, evt.Info.ID)
}
func (evt *MessageInfoWrapper) GetTransactionID() networkid.TransactionID {
@@ -105,7 +91,6 @@ type WAMessageEvent struct {
parsedMessageType string
isUndecryptableUpsertSubEvent bool
- dontRenderEdited bool
postHandle func()
}
@@ -148,6 +133,14 @@ 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
}
@@ -166,6 +159,13 @@ func (evt *WAMessageEvent) PreHandle(ctx context.Context, portal *bridgev2.Porta
log.Info().Msg("Resyncing group members as it appears to have switched to LID addressing mode")
portal.UpdateInfo(ctx, evt.wa.wrapGroupInfo(ctx, info), evt.wa.UserLogin, nil, time.Time{})
log.Debug().Msg("Finished resyncing after LID change")
+ if evt.Info.Sender.Server == types.DefaultUserServer && evt.Info.SenderAlt.Server == types.HiddenUserServer {
+ evt.Info.Sender, evt.Info.SenderAlt = evt.Info.SenderAlt, evt.Info.Sender
+ log.Debug().
+ Stringer("new_sender", evt.Info.Sender).
+ Stringer("new_sender_alt", evt.Info.SenderAlt).
+ Msg("Overriding sender to LID after resyncing group members")
+ }
}
func (evt *WAMessageEvent) PostHandle(ctx context.Context, portal *bridgev2.Portal) {
@@ -181,15 +181,12 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
}
var editedMsg *waE2E.Message
var previouslyConvertedPart *bridgev2.ConvertedMessagePart
- targetMessage := evt.GetTargetMessage()
- cacheMessage := targetMessage
if evt.isUndecryptableUpsertSubEvent {
// TODO db metadata needs to be updated in this case to remove the error
editedMsg = evt.Message
- cacheMessage = evt.GetID()
} else {
editedMsg = evt.Message.GetProtocolMessage().GetEditedMessage()
- previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, targetMessage)
+ previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, evt.GetTargetMessage())
meta := existing[0].Metadata.(*waid.MessageMetadata)
if slices.Contains(meta.Edits, evt.Info.ID) {
return nil, fmt.Errorf("%w: edit already handled", bridgev2.ErrIgnoringRemoteEvent)
@@ -205,11 +202,9 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
evt.postHandle = func() {
evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), cm, false)
}
- } else if len(cm.Parts) > 0 && cacheMessage != "" {
- evt.wa.Main.AddMediaEditCache(portal, cacheMessage, cm.Parts[0])
}
editPart := cm.Parts[0].ToEditPart(existing[0])
- if evt.isUndecryptableUpsertSubEvent || evt.dontRenderEdited {
+ if evt.isUndecryptableUpsertSubEvent {
if editPart.TopLevelExtra == nil {
editPart.TopLevelExtra = make(map[string]any)
}
@@ -390,9 +385,7 @@ func (evt *WAUndecryptableMessage) GetStreamOrder() int64 {
type WAMediaRetry struct {
*events.MediaRetry
- wa *WhatsAppClient
- senderLID types.JID
- chatLID types.JID
+ wa *WhatsAppClient
}
func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType {
@@ -400,7 +393,7 @@ func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType {
}
func (evt *WAMediaRetry) GetPortalKey() networkid.PortalKey {
- return evt.wa.makeWAPortalKey(pickLID(evt.ChatID, evt.chatLID))
+ return evt.wa.makeWAPortalKey(evt.ChatID)
}
func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context {
@@ -414,23 +407,16 @@ func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context {
func (evt *WAMediaRetry) getRealSender() types.JID {
sender := evt.SenderID
- if sender.IsEmpty() {
- if evt.FromMe {
- if evt.ChatID.Server == types.HiddenUserServer {
- sender = evt.wa.GetLID().ToNonAD()
- } else {
- sender = evt.wa.JID.ToNonAD()
- }
- } else if evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.HiddenUserServer || evt.ChatID.Server == types.BotServer {
- sender = evt.ChatID.ToNonAD()
- }
+ if evt.FromMe {
+ sender = evt.wa.JID.ToNonAD()
+ } else if sender.IsEmpty() && (evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.BotServer) {
+ sender = evt.ChatID.ToNonAD()
}
return sender
}
func (evt *WAMediaRetry) GetSender() bridgev2.EventSender {
- realSender := pickLID(evt.getRealSender(), evt.senderLID)
- return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender)
+ return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.getRealSender())
}
func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID {
diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml
index 564f25e..e441f67 100644
--- a/pkg/connector/example-config.yaml
+++ b/pkg/connector/example-config.yaml
@@ -121,6 +121,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 c55f523..a330a98 100644
--- a/pkg/connector/handlematrix.go
+++ b/pkg/connector/handlematrix.go
@@ -48,12 +48,11 @@ var (
_ bridgev2.DeleteChatHandlingNetworkAPI = (*WhatsAppClient)(nil)
)
-func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (result *bridgev2.MatrixMessageResponse, retErr error) {
+func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (*bridgev2.MatrixMessageResponse, error) {
waMsg, optionMap, err := wa.Main.MsgConv.PollStartToWhatsApp(ctx, msg.Content, msg.ReplyTo, msg.Portal)
if err != nil {
return nil, fmt.Errorf("failed to convert poll vote: %w", err)
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
if err != nil {
return nil, err
@@ -68,21 +67,19 @@ func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridge
return resp, nil
}
-func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (result *bridgev2.MatrixMessageResponse, retErr error) {
+func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (*bridgev2.MatrixMessageResponse, error) {
waMsg, err := wa.Main.MsgConv.PollVoteToWhatsApp(ctx, wa.Client, msg.Content, msg.VoteTo)
if err != nil {
return nil, fmt.Errorf("failed to convert poll vote: %w", err)
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
}
-func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (result *bridgev2.MatrixMessageResponse, retErr error) {
+func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) {
waMsg, req, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, msg.Event, msg.Content, msg.ReplyTo, msg.ThreadRoot, msg.Portal)
if err != nil {
return nil, fmt.Errorf("failed to convert message: %w", err)
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.handleConvertedMatrixMessage(ctx, msg, waMsg, req)
}
@@ -103,23 +100,19 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
if err != nil {
return nil, err
}
- if chatJID.Server == types.DefaultUserServer {
- zerolog.Ctx(ctx).Warn().Stringer("portal_jid", chatJID).Msg("Matrix message received in phone number portal")
- }
if chatJID == types.StatusBroadcastJID && wa.Main.Config.DisableStatusBroadcastSend {
return nil, ErrBroadcastSendDisabled
}
wrappedMsgID := waid.MakeMessageID(chatJID, wa.JID, req.ID)
- wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetLID(), req.ID)
+ 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.GetLID() {
+ if resp.Sender == wa.GetStore().GetLID() && chatJID.Server != types.DefaultUserServer {
pickedMessageID = wrappedMsgID2
msg.RemovePending(networkid.TransactionID(wrappedMsgID))
} else {
@@ -140,17 +133,18 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
}, nil
}
-func (wa *WhatsAppClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
+func (wa *WhatsAppClient) PreHandleMatrixReaction(_ context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return bridgev2.MatrixReactionPreResponse{}, fmt.Errorf("failed to parse portal ID: %w", err)
} else if portalJID == types.StatusBroadcastJID {
return bridgev2.MatrixReactionPreResponse{}, ErrBroadcastReactionUnsupported
}
- sender := wa.GetLID()
- if portalJID.Server == types.DefaultUserServer {
- zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("Matrix reaction received in phone number portal")
- sender = wa.JID
+ sender := wa.JID
+ if portalJID.Server == types.HiddenUserServer ||
+ msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup ||
+ msg.Portal.Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID {
+ sender = wa.GetStore().GetLID()
}
return bridgev2.MatrixReactionPreResponse{
SenderID: waid.MakeUserID(sender),
@@ -159,7 +153,7 @@ func (wa *WhatsAppClient) PreHandleMatrixReaction(ctx context.Context, msg *brid
}, nil
}
-func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (result *database.Reaction, retErr error) {
+func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (*database.Reaction, error) {
messageID, err := waid.ParseMessageID(msg.TargetMessage.ID)
if err != nil {
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
@@ -176,10 +170,9 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
SenderTimestampMS: proto.Int64(msg.Event.Timestamp),
},
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
var req whatsmeow.SendRequestExtra
if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
- reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(ctx, wa.Client, messageID), reactionMsg.ReactionMessage)
+ reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(wa.Client, messageID), reactionMsg.ReactionMessage)
if err != nil {
return nil, fmt.Errorf("failed to encrypt reaction: %w", err)
}
@@ -198,7 +191,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
}, err
}
-func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) (retErr error) {
+func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error {
messageID, err := waid.ParseMessageID(msg.TargetReaction.MessageID)
if err != nil {
return fmt.Errorf("failed to parse target message ID: %w", err)
@@ -222,13 +215,12 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b
extra.ID = types.MessageID(msg.InputTransactionID)
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra)
zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response")
return err
}
-func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) (retErr error) {
+func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) error {
log := zerolog.Ctx(ctx)
var editID types.MessageID
@@ -252,8 +244,6 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M
if err != nil {
return fmt.Errorf("failed to convert message: %w", err)
}
- defer wa.mcTrack(edit, time.Now(), &retErr)
-
convertedEdit := wa.Client.BuildEdit(messageID.Chat, messageID.ID, waMsg)
if edit.OrigSender == nil {
convertedEdit.EditedMessage.Message.ProtocolMessage.TimestampMS = proto.Int64(edit.Event.Timestamp)
@@ -268,7 +258,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M
return err
}
-func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) (retErr error) {
+func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error {
log := zerolog.Ctx(ctx)
messageID, err := waid.ParseMessageID(msg.TargetMessage.ID)
if err != nil {
@@ -280,7 +270,6 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
return fmt.Errorf("failed to parse portal ID: %w", err)
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
revokeMessage := wa.Client.BuildRevoke(messageID.Chat, messageID.Sender, messageID.ID)
extra := whatsmeow.SendRequestExtra{}
@@ -293,7 +282,7 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
return err
}
-func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) {
+func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error {
if !receipt.ReadUpTo.After(receipt.LastRead) {
return nil
}
@@ -322,7 +311,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
if err != nil {
continue
}
- if wa.IsOwnJID(parsed.Sender) {
+ if parsed.Sender.User == wa.GetStore().GetLID().User || parsed.Sender.User == wa.JID.User {
continue
}
var key types.JID
@@ -332,7 +321,6 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
}
messagesToRead[key] = append(messagesToRead[key], parsed.ID)
}
- defer wa.mcTrack(receipt, time.Now(), &retErr)
for messageSender, ids := range messagesToRead {
err = wa.Client.MarkRead(ctx, ids, receipt.Receipt.Timestamp, portalJID, messageSender)
if err != nil {
@@ -342,7 +330,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
return err
}
-func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) (retErr error) {
+func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) error {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
@@ -363,7 +351,6 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.
return nil
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
if wa.Main.Config.SendPresenceOnTyping {
err = wa.updatePresence(ctx, types.PresenceAvailable)
if err != nil {
@@ -375,7 +362,7 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.
var errUnsupportedDisappearingTimer = bridgev2.WrapErrorInStatus(errors.New("invalid value for disappearing timer")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true)
-func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (ok bool, retErr error) {
+func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (bool, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
@@ -387,7 +374,6 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
return false, fmt.Errorf("%w (%s)", errUnsupportedDisappearingTimer, msg.Content.Timer.Duration)
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
settingTS := time.UnixMilli(msg.Event.Timestamp)
err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS)
if err != nil {
@@ -404,11 +390,7 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
return true, nil
}
-func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (result *bridgev2.MatrixMembershipResult, retErr error) {
- if msg.Type.IsSelf && msg.OrigSender != nil {
- return nil, nil
- }
-
+func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return nil, err
@@ -448,7 +430,6 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target)
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action)
if err != nil {
return nil, err
@@ -464,7 +445,7 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
return &bridgev2.MatrixMembershipResult{RedirectTo: waid.MakeUserID(resp[0].JID)}, nil
}
-func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (ok bool, retErr error) {
+func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (bool, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
@@ -474,7 +455,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev
return false, fmt.Errorf("cannot set room name for DM")
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name)
if err != nil {
return false, err
@@ -486,7 +466,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev
return true, nil
}
-func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (ok bool, retErr error) {
+func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (bool, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
@@ -496,7 +476,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge
return false, fmt.Errorf("cannot set room topic for DM")
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
newID := wa.Client.GenerateMessageID()
oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID
err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic)
@@ -511,7 +490,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge
return true, nil
}
-func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (ok bool, retErr error) {
+func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (bool, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
@@ -534,7 +513,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridg
}
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
avatarID, err := wa.Client.SetGroupPhoto(ctx, portalJID, data)
if err != nil {
return false, err
@@ -602,7 +580,7 @@ func convertRoomAvatar(data []byte) ([]byte, error) {
return buf.Bytes(), nil
}
-func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMute) (retErr error) {
+func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMute) error {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
@@ -613,16 +591,14 @@ func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMu
if !muted || mutedUntil == event.MutedForever {
muteTS = nil
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.Client.SendAppState(ctx, appstate.BuildMuteAbs(chatJID, muted, muteTS))
}
-func (wa *WhatsAppClient) HandleRoomTag(ctx context.Context, msg *bridgev2.MatrixRoomTag) (retErr error) {
+func (wa *WhatsAppClient) HandleRoomTag(ctx context.Context, msg *bridgev2.MatrixRoomTag) error {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
_, isFavorite := msg.Content.Tags[event.RoomTagFavourite]
return wa.Client.SendAppState(ctx, appstate.BuildPin(chatJID, isFavorite))
}
@@ -638,7 +614,7 @@ func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types.
lastTS = msgs[0].Timestamp
parsed, _ := waid.ParseMessageID(msgs[0].ID)
if parsed != nil {
- fromMe := wa.IsOwnJID(parsed.Sender)
+ fromMe := parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.GetStore().GetLID().ToNonAD()
var participant *string
if chatJID.Server == types.GroupServer {
participant = ptr.Ptr(parsed.Sender.String())
@@ -654,7 +630,7 @@ func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types.
return lastTS, lastKey, nil
}
-func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.MatrixMarkedUnread) (retErr error) {
+func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.MatrixMarkedUnread) error {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
@@ -663,11 +639,10 @@ func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.
if err != nil {
return err
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.Client.SendAppState(ctx, appstate.BuildMarkChatAsRead(chatJID, msg.Content.Unread, lastTS, lastKey))
}
-func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) (retErr error) {
+func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
@@ -688,6 +663,5 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg
if err != nil {
return err
}
- defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true))
}
diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go
index 509c589..eb42f43 100644
--- a/pkg/connector/handlewhatsapp.go
+++ b/pkg/connector/handlewhatsapp.go
@@ -28,6 +28,7 @@ import (
"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"
@@ -76,7 +77,6 @@ func init() {
func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
log := wa.UserLogin.Log
ctx := log.WithContext(wa.Main.Bridge.BackgroundCtx)
- wa.MC.OnWhatsAppEvent(rawEvt)
success = true
switch evt := rawEvt.(type) {
@@ -113,7 +113,8 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
case *events.HistorySync:
wa.UserLogin.Log.Warn().Msg("Unexpected history sync event received")
case *events.MediaRetry:
- success = wa.handleWAMediaRetry(ctx, evt)
+ wa.phoneSeen(evt.Timestamp)
+ success = wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{MediaRetry: evt, wa: wa}).Success
case *events.GroupInfo:
success = wa.handleWAGroupInfoChange(ctx, evt)
@@ -125,20 +126,6 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
success = wa.handleWANewsletterLeave(evt)
case *events.Picture:
success = wa.handleWAPictureUpdate(ctx, evt)
- case *events.NotifyAccountReachoutTimelock:
- wa.UserLogin.TrackAnalytics("WhatsApp Account Reachout Timelock", map[string]any{
- "enforcement_type": evt.EnforcementType,
- "is_active": evt.IsActive,
- "time_enforcement_ends": evt.TimeEnforcementEnds.Time,
- })
- wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil = evt.TimeEnforcementEnds.Time
- if wa.UserLogin.BridgeState.GetPrevUnsent().StateEvent == status.StateConnected {
- wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
- }
- err := wa.UserLogin.Save(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to save user login metadata after reachout timelock update")
- }
case *events.AppStateSyncComplete:
wa.handleWAAppStateSyncComplete(ctx, evt)
@@ -157,7 +144,7 @@ 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.GetLID().ToNonAD(), evt.Action.GetName())
+ _, _, 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")
}
@@ -181,6 +168,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
}()
go wa.syncRemoteProfile(ctx, nil)
}
+ wa.MC.OnConnect(store.GetWAVersion()[2], wa.Device.Platform)
case *events.OfflineSyncPreview:
log.Info().
Int("message_count", evt.Messages).
@@ -258,40 +246,115 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
return
}
-func (wa *WhatsAppClient) ensureAltJIDs(ctx context.Context, info *types.MessageSource, checkPhones bool) bool {
- var err error
- if info.Sender.Server == types.DefaultUserServer && info.SenderAlt.IsEmpty() {
- info.SenderAlt, err = wa.GetStore().LIDs.GetLIDForPN(ctx, info.Sender)
+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)
+ }
+ 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).
+ Msg("Forced LID DM sender to phone number in incoming message")
+ info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
+ info.Chat = info.Sender.ToNonAD()
+ } else if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.Server == types.DefaultUserServer {
+ wa.UserLogin.Log.Debug().
+ Stringer("lid", info.Chat).
+ Stringer("pn", info.RecipientAlt).
+ Any("message_id", msgID).
+ Str("evt_type", evtType).
+ 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 {
- zerolog.Ctx(ctx).Err(err).Stringer("sender", info.Sender).Msg("Failed to get LID for sender")
- return false
+ wa.UserLogin.Log.Err(err).
+ Any("message_id", msgID).
+ 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).
+ Msg("Forced LID chat to phone number in bot message")
+ info.Chat = chatPN
}
}
- if info.Chat.Server == types.DefaultUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() {
- info.RecipientAlt, err = wa.GetStore().LIDs.GetLIDForPN(ctx, info.Chat)
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Stringer("chat", info.Chat).Msg("Failed to get LID for chat")
- return false
- }
- }
- if checkPhones {
- return wa.checkAllPhonesInMessage(ctx, info)
- }
- return true
}
func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) {
success = true
+ 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.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
return
}
- if evt.Info.Chat.Server == types.NewsletterServer && wa.disableNewsletter {
+ 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,
+ }
+ } 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
}
- if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) {
- return false
- }
+
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 {
@@ -313,65 +376,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.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
- }
-
- dontRenderEdited := false
- messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation()
- if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD {
- parentKey := messageAssoc.GetParentMessageKey()
- protocolMsg := evt.Message.GetProtocolMessage()
- if protocolMsg.GetType() != waE2E.ProtocolMessage_MESSAGE_EDIT || protocolMsg.GetKey() == nil {
- protocolMsg = &waE2E.ProtocolMessage{
- Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(),
- Key: parentKey,
- EditedMessage: evt.Message.GetAssociatedChildMessage().GetMessage(),
- }
- dontRenderEdited = true
- } else if child := protocolMsg.GetEditedMessage().GetAssociatedChildMessage().GetMessage(); child != nil {
- protocolMsg.EditedMessage = child
- protocolMsg.Key = parentKey
- }
- 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")
-
- evt.Message = &waE2E.Message{
- ProtocolMessage: protocolMsg,
- }
- parsedMessageType = getMessageType(evt.Message)
- } 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,
@@ -381,15 +392,12 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
MsgEvent: evt,
parsedMessageType: parsedMessageType,
- dontRenderEdited: dontRenderEdited,
})
return res.Success
}
func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool {
- if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) {
- return false
- }
+ wa.rerouteWAMessage(ctx, "undecryptable message", &evt.Info.MessageSource, evt.Info.ID)
wa.UserLogin.Log.Debug().
Any("info", evt.Info).
Bool("unavailable", evt.IsUnavailable).
@@ -412,44 +420,12 @@ func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt
return res.Success
}
-func (wa *WhatsAppClient) handleWAMediaRetry(ctx context.Context, evt *events.MediaRetry) bool {
- wa.phoneSeen(evt.Timestamp)
- var senderLID, chatLID types.JID
- var err error
- if evt.SenderID.Server == types.DefaultUserServer {
- senderLID, err = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.SenderID)
- if err != nil {
- wa.UserLogin.Log.Err(err).
- Stringer("sender_id", evt.SenderID).
- Msg("Failed to get LID for media retry sender")
- return false
- }
- }
- if evt.ChatID.Server == types.DefaultUserServer {
- chatLID, err = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.ChatID)
- if err != nil {
- wa.UserLogin.Log.Err(err).
- Stringer("chat_id", evt.ChatID).
- Msg("Failed to get LID for media retry chat")
- return false
- }
- }
- res := wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{
- MediaRetry: evt,
- wa: wa,
- senderLID: senderLID,
- chatLID: chatLID,
- })
- return res.Success
-}
-
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.IsFromMe && evt.Sender.Device == 0 {
wa.phoneSeen(evt.Timestamp)
}
- if !wa.ensureAltJIDs(ctx, &evt.MessageSource, true) {
- return false
- }
var evtType bridgev2.RemoteEventType
switch evt.Type {
case types.ReceiptTypeRead, types.ReceiptTypeReadSelf:
@@ -461,34 +437,28 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
default:
return true
}
- targets := make([]networkid.MessageID, 0, len(evt.MessageIDs))
- messageSender := wa.GetLID()
+ targets := make([]networkid.MessageID, len(evt.MessageIDs))
+ messageSender := wa.JID
if !evt.MessageSender.IsEmpty() {
messageSender = evt.MessageSender
- }
- var chatAlt types.JID
- if evt.Chat.Server == types.DefaultUserServer {
- chatLID, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Chat)
- if !chatLID.IsEmpty() {
- chatAlt = evt.Chat
- evt.Chat = chatLID
+ // 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() {
+ messageSender = lid
}
}
- for _, id := range evt.MessageIDs {
- targets = append(targets, waid.MakeMessageID(evt.Chat, messageSender, id))
- if !chatAlt.IsEmpty() {
- targets = append(targets, waid.MakeMessageID(chatAlt, messageSender, id))
- }
- }
- senderLID := evt.Sender
- if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() {
- senderLID = evt.SenderAlt
+ for i, id := range evt.MessageIDs {
+ targets[i] = waid.MakeMessageID(evt.Chat, messageSender, id)
}
res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{
EventMeta: simplevent.EventMeta{
Type: evtType,
PortalKey: wa.makeWAPortalKey(evt.Chat),
- Sender: wa.makeEventSender(ctx, senderLID),
+ Sender: wa.makeEventSender(ctx, evt.Sender),
Timestamp: evt.Timestamp,
},
Targets: targets,
@@ -497,11 +467,11 @@ 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.DefaultUserServer && evt.Sender.ToNonAD() == evt.Chat {
+ if evt.Chat.Server == types.HiddenUserServer && evt.Sender.ToNonAD() == evt.Chat {
if evt.SenderAlt.IsEmpty() {
- evt.SenderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Sender)
+ evt.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, evt.Sender)
}
- if evt.SenderAlt.Server == types.HiddenUserServer {
+ if evt.SenderAlt.Server == types.DefaultUserServer {
evt.Sender, evt.SenderAlt = evt.SenderAlt, evt.Sender
evt.Chat = evt.Sender.ToNonAD()
}
@@ -538,7 +508,6 @@ func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onC
wa.Disconnect()
wa.Client = nil
wa.JID = types.EmptyJID
- wa.LID = types.EmptyJID
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0
wa.UserLogin.BridgeState.Send(status.BridgeState{
StateEvent: status.StateBadCredentials,
@@ -552,15 +521,12 @@ func (wa *WhatsAppClient) handleWACallStart(ctx context.Context, group, sender,
if !wa.Main.Config.CallStartNotices || time.Since(ts) > callEventMaxAge {
return true
}
- if sender.Server == types.DefaultUserServer && senderAlt.IsEmpty() {
- senderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, sender)
- }
- if sender.Server == types.DefaultUserServer && senderAlt.Server == types.HiddenUserServer {
+ if sender.Server == types.HiddenUserServer && senderAlt.Server == types.DefaultUserServer {
wa.UserLogin.Log.Debug().
- Stringer("lid", senderAlt).
- Stringer("pn", sender).
+ Stringer("lid", sender).
+ Stringer("pn", senderAlt).
Str("call_id", id).
- Msg("Forced phone number caller to LID in incoming call")
+ Msg("Forced LID caller to phone number in incoming call")
sender, senderAlt = senderAlt, sender
}
chat := group
@@ -607,12 +573,6 @@ func (wa *WhatsAppClient) handleWAIdentityChange(ctx context.Context, evt *event
if !wa.Main.Config.IdentityChangeNotices {
return
}
- if evt.JID.Server == types.DefaultUserServer {
- lid, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.JID)
- if !lid.IsEmpty() {
- evt.JID = lid
- }
- }
wa.UserLogin.QueueRemoteEvent(&simplevent.Message[*events.IdentityChange]{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessage,
@@ -663,14 +623,13 @@ func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.De
func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool {
chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID)
- senderJID := wa.maybeConvertJIDToLID(ctx, evt.SenderJID)
return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessageRemove,
PortalKey: wa.makeWAPortalKey(chatJID),
Timestamp: evt.Timestamp,
},
- TargetMessage: waid.MakeMessageID(chatJID, senderJID, evt.MessageID),
+ TargetMessage: waid.MakeMessageID(chatJID, evt.SenderJID, evt.MessageID),
OnlyForMe: true,
}).Success
}
@@ -681,7 +640,7 @@ func (wa *WhatsAppClient) handleWAMarkChatAsRead(ctx context.Context, evt *event
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventReadReceipt,
PortalKey: wa.makeWAPortalKey(chatJID),
- Sender: wa.makeEventSender(ctx, wa.GetLID()),
+ Sender: wa.makeEventSender(ctx, wa.JID),
Timestamp: evt.Timestamp,
},
ReadUpTo: evt.Timestamp,
@@ -770,6 +729,9 @@ func (wa *WhatsAppClient) handleWAGroupInfoChange(ctx context.Context, evt *even
}
func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.JoinedGroup) bool {
+ if wa.createDedup.Pop(evt.CreateKey) {
+ return true
+ }
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatResync,
@@ -782,9 +744,6 @@ func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.J
}
func (wa *WhatsAppClient) handleWANewsletterJoin(ctx context.Context, evt *events.NewsletterJoin) bool {
- if wa.disableNewsletter {
- return true
- }
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatResync,
@@ -883,12 +842,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt
} else {
log.Info().
Time("recovery_ts", ts).
- Bool("recovery_evt", evt.Recovery).
Msg("Unmarked app state recovery as attempted after successful full sync")
- wa.UserLogin.TrackAnalytics("WhatsApp Appstate Recovery Success", map[string]any{
- "patch_name": evt.Name,
- "from_recovery": evt.Recovery,
- })
}
} else if ts, exists = wa.appStateFullSyncAttempted[evt.Name]; exists {
delete(wa.appStateFullSyncAttempted, evt.Name)
@@ -945,9 +899,6 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev
if err != nil {
log.Err(err).Msg("Failed to save login metadata after marking app state recovery as attempted")
}
- wa.UserLogin.TrackAnalytics("WhatsApp Appstate Recovery Request", map[string]any{
- "patch_name": evt.Name,
- })
go func() {
resp, err := wa.Client.SendPeerMessage(ctx, whatsmeow.BuildAppStateRecoveryRequest(evt.Name))
if err != nil {
diff --git a/pkg/connector/id.go b/pkg/connector/id.go
index c0aa06b..c07e431 100644
--- a/pkg/connector/id.go
+++ b/pkg/connector/id.go
@@ -49,7 +49,7 @@ func (wa *WhatsAppClient) makeEventSender(ctx context.Context, id types.JID) bri
}
}
return bridgev2.EventSender{
- IsFromMe: wa.IsOwnJID(id),
+ IsFromMe: id.User == wa.GetStore().GetJID().User || id.User == wa.GetStore().GetLID().User,
Sender: waid.MakeUserID(id),
SenderLogin: waid.MakeUserLoginID(senderLoginJID),
}
@@ -60,25 +60,24 @@ func (wa *WhatsAppClient) messageIDToKey(id *waid.ParsedMessageID) *waCommon.Mes
RemoteJID: ptr.Ptr(id.Chat.String()),
ID: ptr.Ptr(id.ID),
}
- if wa.IsOwnJID(id.Sender) {
+ if id.Sender.User == wa.GetStore().GetJID().User || id.Sender.User == wa.GetStore().GetLID().User {
key.FromMe = ptr.Ptr(true)
}
- if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer &&
- id.Chat.Server != types.HiddenUserServer && id.Chat.Server != types.BotServer {
+ if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer && id.Chat.Server != types.HiddenUserServer && id.Chat.Server != types.BotServer {
key.Participant = ptr.Ptr(id.Sender.String())
}
return key
}
-func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, jid types.JID) types.JID {
- if jid.Server == types.DefaultUserServer {
- if lidForPN, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid); err != nil {
+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("pn", jid).
- Msg("Failed to get LID for phone number chat")
- } else if !lidForPN.IsEmpty() {
- return lidForPN
+ Stringer("lid", chatJID).
+ Msg("Failed to get phone number for LID chat")
+ } else if !pn.IsEmpty() {
+ return pn.ToNonAD()
}
}
- return jid
+ return chatJID
}
diff --git a/pkg/connector/lidmigrate.go b/pkg/connector/lidmigrate.go
deleted file mode 100644
index cfab099..0000000
--- a/pkg/connector/lidmigrate.go
+++ /dev/null
@@ -1,212 +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
%s`, html.EscapeString(url), html.EscapeString(name), html.EscapeString(msg.GetAddress())),
+ FormattedBody: fmt.Sprintf("Location: %s
%s", url, name, msg.GetAddress()),
GeoURI: fmt.Sprintf("geo:%.5f,%.5f", msg.GetDegreesLatitude(), msg.GetDegreesLongitude()),
}
diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go
index 9964800..9a1ceb3 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,19 +26,21 @@ import (
"io"
"net/http"
"os"
+ "path/filepath"
+ "strconv"
"strings"
"github.com/rs/zerolog"
- "go.mau.fi/util/exfmt"
"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"
)
@@ -83,15 +87,15 @@ 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)
- preparedMedia.URL, err = portal.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver, getMediaIDVersion(msg)))
+ preparedMedia.URL, err = portal.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver))
if err != nil {
panic(fmt.Errorf("failed to generate content URI: %w", err))
}
@@ -120,20 +124,13 @@ func (mc *MessageConverter) convertMediaMessage(
return
}
-func getMediaIDVersion(msg MediaMessage) []byte {
- if encSHA256 := msg.GetFileEncSHA256(); len(encSHA256) > 0 {
- return encSHA256
- }
- return msg.GetFileSHA256()
-}
-
func (mc *MessageConverter) convertAlbumMessage(ctx context.Context, msg *waE2E.AlbumMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
parts := make([]string, 0, 2)
if msg.GetExpectedImageCount() > 0 {
- parts = append(parts, exfmt.Pluralizable("image")(int(msg.GetExpectedImageCount())))
+ parts = append(parts, fmt.Sprintf("%d images", msg.GetExpectedImageCount()))
}
if msg.GetExpectedVideoCount() > 0 {
- parts = append(parts, exfmt.Pluralizable("video")(int(msg.GetExpectedVideoCount())))
+ parts = append(parts, fmt.Sprintf("%d videos", msg.GetExpectedVideoCount()))
}
var partDesc string
if len(parts) > 0 {
@@ -201,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
@@ -244,19 +239,6 @@ type MediaMessageWithDuration interface {
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{
@@ -305,7 +287,19 @@ 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"
+ }
+ if data.Info.Width == data.Info.Height {
+ data.Info.Width = WhatsAppStickerSize
+ data.Info.Height = WhatsAppStickerSize
+ } else if data.Info.Width > data.Info.Height {
+ data.Info.Height /= data.Info.Width / WhatsAppStickerSize
+ data.Info.Width = WhatsAppStickerSize
+ } else {
+ data.Info.Width /= data.Info.Height / WhatsAppStickerSize
+ data.Info.Height = WhatsAppStickerSize
+ }
case *waE2E.VideoMessage:
data.MsgType = event.MsgVideo
pairedMediaType := msg.GetContextInfo().GetPairedMediaType()
@@ -365,17 +359,14 @@ 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.ErrInvalidMediaSHA256) {
+ if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
} else if err != nil {
return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
@@ -396,7 +387,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
}
} else {
data, err := client.Download(ctx, message)
- if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
+ if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
} else if err != nil {
return fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
@@ -406,14 +397,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)
}
@@ -422,7 +411,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,
@@ -436,6 +425,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
@@ -480,3 +531,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 fa495e0..22c014c 100644
--- a/pkg/msgconv/wa-misc.go
+++ b/pkg/msgconv/wa-misc.go
@@ -20,14 +20,12 @@ import (
"context"
"encoding/base64"
"fmt"
- "html"
"html/template"
"strings"
"time"
"github.com/rs/zerolog"
"go.mau.fi/util/exerrors"
- "go.mau.fi/util/exfmt"
"go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow/proto/waAICommonDeprecated"
"go.mau.fi/whatsmeow/proto/waE2E"
@@ -75,6 +73,72 @@ func (mc *MessageConverter) convertPlaceholderMessage(ctx context.Context, rawMs
}
}
+func (mc *MessageConverter) getHistoryReceiverName(ctx context.Context, receiver string) string {
+ jid, err := types.ParseJID(receiver)
+ if err != nil {
+ zerolog.Ctx(ctx).Err(err).Str("receiver_jid", receiver).Msg("Failed to parse message history receiver JID")
+ return receiver
+ }
+ _, displayname, err := mc.getBasicUserInfo(ctx, jid)
+ if err != nil {
+ zerolog.Ctx(ctx).Err(err).Stringer("receiver_jid", jid).Msg("Failed to resolve message history receiver")
+ if jid.User != "" {
+ return jid.User
+ }
+ return receiver
+ }
+ return displayname
+}
+
+func messageHistoryStartTime(metadata *waE2E.MessageHistoryMetadata, fallback time.Time) time.Time {
+ if metadata == nil {
+ return fallback
+ }
+ if ts := metadata.GetOldestMessageTimestamp(); ts > 0 {
+ return time.Unix(ts, 0).Local()
+ }
+ return fallback
+}
+
+func (mc *MessageConverter) convertMessageHistoryShare(ctx context.Context, info *types.MessageInfo, metadata *waE2E.MessageHistoryMetadata, contextInfo *waE2E.ContextInfo) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
+ names := make([]string, 0, len(metadata.GetHistoryReceivers()))
+ for _, receiver := range metadata.GetHistoryReceivers() {
+ if name := mc.getHistoryReceiverName(ctx, receiver); name != "" {
+ names = append(names, name)
+ }
+ }
+
+ receivers := strings.Join(names, ", ")
+ var fallback time.Time
+ if info != nil {
+ fallback = info.Timestamp
+ }
+ startAt := messageHistoryStartTime(metadata, fallback)
+ body := "Message history shared."
+ if !startAt.IsZero() {
+ startTime := startAt.Format("Jan 2, 2006 at 3:04 PM")
+ body = fmt.Sprintf("Message history shared starting on %s.", startTime)
+ switch {
+ case info != nil && info.IsFromMe && receivers != "":
+ body = fmt.Sprintf("You sent %s message history that starts on %s.", receivers, startTime)
+ case receivers != "":
+ body = fmt.Sprintf("Sent %s message history that starts on %s.", receivers, startTime)
+ }
+ } else if info != nil && info.IsFromMe && receivers != "" {
+ body = fmt.Sprintf("You sent %s message history.", receivers)
+ } else if receivers != "" {
+ body = fmt.Sprintf("Sent %s message history.", receivers)
+ }
+
+ return &bridgev2.ConvertedMessagePart{
+ Type: event.EventMessage,
+ Content: &event.MessageEventContent{
+ MsgType: event.MsgNotice,
+ Body: body,
+ },
+ }, contextInfo
+}
+
const inviteMsg = `%s
%s accept to accept the invite.`
const inviteMsgBroken = `%s