From 5250ddfa7d6b92228aff3f29d18b40795e7709d8 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 10 Jul 2026 18:36:17 +0300 Subject: [PATCH 001/139] msgconv/from-whatsapp: handle v5 and v6 poll creations --- pkg/connector/wamsgtype.go | 7 ++++--- pkg/msgconv/from-whatsapp.go | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/connector/wamsgtype.go b/pkg/connector/wamsgtype.go index 4bcf483..0a29b82 100644 --- a/pkg/connector/wamsgtype.go +++ b/pkg/connector/wamsgtype.go @@ -67,10 +67,11 @@ func getMessageType(waMsg *waE2E.Message) string { return "encrypted comment" case waMsg.CommentMessage != nil: return "comment" - case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil: + case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil || + waMsg.PollCreationMessageV5 != nil || waMsg.PollCreationMessageV6 != nil: return "poll create" - case waMsg.PollCreationMessageV4 != nil || waMsg.PollCreationMessageV5 != nil: - return "poll create (vNext)" + case waMsg.PollCreationMessageV4 != nil: + return "poll create (v4)" case waMsg.PollUpdateMessage != nil: return "poll update" case waMsg.ProtocolMessage != nil: diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go index 6eea25f..e8cb032 100644 --- a/pkg/msgconv/from-whatsapp.go +++ b/pkg/msgconv/from-whatsapp.go @@ -176,6 +176,12 @@ func (mc *MessageConverter) ToMatrix( part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV2) case waMsg.PollCreationMessageV3 != nil: part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV3) + //case waMsg.PollCreationMessageV4 != nil: + // part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV4) + case waMsg.PollCreationMessageV5 != nil: + part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV5) + case waMsg.PollCreationMessageV6 != nil: + part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV6) case waMsg.PollUpdateMessage != nil: part, contextInfo = mc.convertPollUpdateMessage(ctx, info, origSource, waMsg.PollUpdateMessage) case waMsg.EventMessage != nil: From c2fc0d0b81d23ecadd54cbeb58c62a75df7824d0 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 13 Jul 2026 14:21:42 +0300 Subject: [PATCH 002/139] dependencies: update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 35a6655..7c706b5 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/tidwall/gjson v1.19.0 go.mau.fi/util v0.9.10 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260709092057-73fe7355f59f + go.mau.fi/whatsmeow v0.0.0-20260713112039-592721a00b51 golang.org/x/image v0.42.0 golang.org/x/net v0.56.0 golang.org/x/sync v0.21.0 diff --git a/go.sum b/go.sum index 3d0391c..f7bf881 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg= go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A= 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-20260709092057-73fe7355f59f h1:VZkwFBEQ9TbB9IWsldw8BJDhHbEBElm9VD2VQXbSc9s= -go.mau.fi/whatsmeow v0.0.0-20260709092057-73fe7355f59f/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U= +go.mau.fi/whatsmeow v0.0.0-20260713112039-592721a00b51 h1:KkppXopVRx6Juio9FmPBVQzzK0e1uW65ynIDBi/v4c8= +go.mau.fi/whatsmeow v0.0.0-20260713112039-592721a00b51/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U= 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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= From ba6430c70131eab3265b80e854bc9e77181618e6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 13 Jul 2026 14:22:00 +0300 Subject: [PATCH 003/139] startchat: add cache to avoid repeated IsOnWhatsApp calls --- pkg/connector/startchat.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/connector/startchat.go b/pkg/connector/startchat.go index a0b0692..fb2ce16 100644 --- a/pkg/connector/startchat.go +++ b/pkg/connector/startchat.go @@ -25,6 +25,7 @@ import ( "time" "github.com/rs/zerolog" + "go.mau.fi/util/exsync" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/types" @@ -62,6 +63,13 @@ func looksEmaily(str string) bool { return false } +type cacheEntry struct { + jid types.JID + ts time.Time +} + +var isOnWhatsappCache = exsync.NewMap[string, cacheEntry]() + func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) (types.JID, error) { if strings.HasSuffix(number, "@"+types.BotServer) || strings.HasSuffix(number, "@"+types.HiddenUserServer) { return types.ParseJID(number) @@ -76,6 +84,8 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) return types.EmptyJID, ErrInputLooksLikeEmail } else if wa.Client == nil || !wa.Client.IsLoggedIn() { return types.EmptyJID, bridgev2.ErrNotLoggedIn + } else if entry, ok := isOnWhatsappCache.Get(number); ok && time.Since(entry.ts) < 4*time.Hour { + return entry.jid, nil } else if resp, err := wa.Client.IsOnWhatsApp(ctx, []string{number}); err != nil { return types.EmptyJID, fmt.Errorf("failed to check if number is on WhatsApp: %w", err) } else if len(resp) == 0 { @@ -83,6 +93,7 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) } else if !resp[0].IsIn { return types.EmptyJID, bridgev2.WrapRespErr(fmt.Errorf("the server said +%s is not on WhatsApp", resp[0].JID.User), mautrix.MNotFound) } else { + isOnWhatsappCache.Set(number, cacheEntry{resp[0].JID, time.Now()}) return resp[0].JID, nil } } From 316a65bae093daada204cc62a8d19346c2569f6a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 13 Jul 2026 14:22:47 +0300 Subject: [PATCH 004/139] changelog: update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 560e2b5..37b9fe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v26.07 (unreleased) + +* 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. From e0d37d9fa94684955bcca97b202bdf27a06789aa Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 13 Jul 2026 14:25:04 +0300 Subject: [PATCH 005/139] msgconv/matrixpoll: assume DMs are always sent to LIDs --- pkg/msgconv/matrixpoll.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/msgconv/matrixpoll.go b/pkg/msgconv/matrixpoll.go index b53c22f..a5f02ea 100644 --- a/pkg/msgconv/matrixpoll.go +++ b/pkg/msgconv/matrixpoll.go @@ -149,7 +149,7 @@ func (mc *MessageConverter) PollVoteToWhatsApp( func MessageIDToInfo(ctx context.Context, client *whatsmeow.Client, parsedMsgID *waid.ParsedMessageID) *types.MessageInfo { chat := parsedMsgID.Chat sender := parsedMsgID.Sender - if client.Store.LIDMigrationTimestamp != 0 && chat.Server == types.DefaultUserServer { + if chat.Server == types.DefaultUserServer { chatLID, _ := client.Store.LIDs.GetLIDForPN(ctx, chat) senderLID, _ := client.Store.LIDs.GetLIDForPN(ctx, sender) if !chatLID.IsEmpty() && !senderLID.IsEmpty() { From 70c7325bb270fde43d0ef6d5b5ae72e7aceda662 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 13 Jul 2026 14:30:21 +0300 Subject: [PATCH 006/139] dependencies: update whatsmeow again --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7c706b5..ba3792b 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/tidwall/gjson v1.19.0 go.mau.fi/util v0.9.10 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260713112039-592721a00b51 + go.mau.fi/whatsmeow v0.0.0-20260713112832-d8960d9575d2 golang.org/x/image v0.42.0 golang.org/x/net v0.56.0 golang.org/x/sync v0.21.0 diff --git a/go.sum b/go.sum index f7bf881..f24a245 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg= go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A= 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-20260713112039-592721a00b51 h1:KkppXopVRx6Juio9FmPBVQzzK0e1uW65ynIDBi/v4c8= -go.mau.fi/whatsmeow v0.0.0-20260713112039-592721a00b51/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U= +go.mau.fi/whatsmeow v0.0.0-20260713112832-d8960d9575d2 h1:2J3waQ2Br8Mh3SW5tVuuChLysXKeprGcTonORU51CIo= +go.mau.fi/whatsmeow v0.0.0-20260713112832-d8960d9575d2/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U= 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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= From 06a93a559d4f0e2b8744afa8b4a8a42edfbfa10d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 14 Jul 2026 22:22:21 +0300 Subject: [PATCH 007/139] docker: update to Alpine 3.24 --- Dockerfile | 4 ++-- Dockerfile.ci | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4efc9d5..e59d110 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1-alpine3.23 AS builder +FROM golang:1-alpine3.24 AS builder RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev @@ -6,7 +6,7 @@ COPY . /build WORKDIR /build RUN ./build.sh -FROM alpine:3.23 +FROM alpine:3.24 ENV UID=1337 \ GID=1337 diff --git a/Dockerfile.ci b/Dockerfile.ci index cb3be51..0a73ff5 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1,6 +1,6 @@ ARG DOCKER_HUB="docker.io" -FROM ${DOCKER_HUB}/alpine:3.23 +FROM ${DOCKER_HUB}/alpine:3.24 ENV UID=1337 \ GID=1337 From a86f5eb9bf7d5a4a6cc7a1c4e42d322bdcb03aa2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 16 Jul 2026 14:15:56 +0300 Subject: [PATCH 008/139] Bump version to v26.07 --- CHANGELOG.md | 3 +- cmd/mautrix-whatsapp/main.go | 2 +- go.mod | 30 +++++++++---------- go.sum | 56 ++++++++++++++++++------------------ 4 files changed, 46 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b9fe8..e47e7b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ -# v26.07 (unreleased) +# 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. diff --git a/cmd/mautrix-whatsapp/main.go b/cmd/mautrix-whatsapp/main.go index 07baaec..47d971c 100644 --- a/cmd/mautrix-whatsapp/main.go +++ b/cmd/mautrix-whatsapp/main.go @@ -18,7 +18,7 @@ var m = mxmain.BridgeMain{ Name: "mautrix-whatsapp", URL: "https://github.com/mautrix/whatsapp", Description: "A Matrix-WhatsApp puppeting bridge.", - Version: "26.06", + Version: "26.07", SemCalVer: true, Connector: &connector.WhatsAppConnector{}, } diff --git a/go.mod b/go.mod index ba3792b..16504a2 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module go.mau.fi/mautrix-whatsapp go 1.25.0 -toolchain go1.26.4 +toolchain go1.26.5 tool go.mau.fi/util/cmd/maubuild @@ -10,15 +10,15 @@ 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.9.10 + go.mau.fi/util v0.9.11 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260713112832-d8960d9575d2 - golang.org/x/image v0.42.0 - golang.org/x/net v0.56.0 - golang.org/x/sync v0.21.0 + go.mau.fi/whatsmeow v0.0.0-20260716095330-85d99080dee8 + golang.org/x/image v0.44.0 + golang.org/x/net v0.57.0 + golang.org/x/sync v0.22.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895 + maunium.net/go/mautrix v0.29.0 ) require ( @@ -31,8 +31,8 @@ require ( 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.45 // indirect - github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect + github.com/mattn/go-sqlite3 v1.14.48 // indirect + github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rs/xid v1.6.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect @@ -40,14 +40,14 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect - github.com/yuin/goldmark v1.8.2 // indirect + github.com/yuin/goldmark v1.8.4 // indirect go.mau.fi/libsignal v0.2.2 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect - golang.org/x/crypto v0.53.0 // indirect - golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect maunium.net/go/mauflag v1.0.0 // indirect diff --git a/go.sum b/go.sum index f24a245..7899436 100644 --- a/go.sum +++ b/go.sum @@ -34,10 +34,10 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk= -github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= -github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM= -github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= +github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= +github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca h1:GHSUVE4yOgX4E7kTRzpxCPbCOYkd3Kj8Dgdod30OI1E= +github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -67,35 +67,35 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= -github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= -github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= +github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU= go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0= -go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg= -go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A= +go.mau.fi/util v0.9.11 h1:Cus1Lu/t7d3OG6VF4aYWvlUUS0Q4O1/lcpPNJZ0jsw0= +go.mau.fi/util v0.9.11/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= -go.mau.fi/whatsmeow v0.0.0-20260713112832-d8960d9575d2 h1:2J3waQ2Br8Mh3SW5tVuuChLysXKeprGcTonORU51CIo= -go.mau.fi/whatsmeow v0.0.0-20260713112832-d8960d9575d2/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U= +go.mau.fi/whatsmeow v0.0.0-20260716095330-85d99080dee8 h1:7RQA3v4pCZcmgHaEQXKfHKLVSqPThizkApt6Uw+DcA8= +go.mau.fi/whatsmeow v0.0.0-20260716095330-85d99080dee8/go.mod h1:SX7VdCALDRNx7HZ7mqZxjyi3U7N0QtbSXXXTl8rG5S4= 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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= -golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY= -golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g= +golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= +golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= +golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895 h1:oe87/mP1vo4LnA6CJDEfyg5ZiAOar2quJNypfO8eS4Y= -maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY= +maunium.net/go/mautrix v0.29.0 h1:OkcBJF1dvp+93EgahxMxOUZZOrGTYculI9IprvRIMOQ= +maunium.net/go/mautrix v0.29.0/go.mod h1:LynuVr8N9nWsE1N4WAE+vItRACDB1pt9M3gN4SIBpeY= From 13c2470aeef71af155a381078f135fc01a93fc8b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 18 Jul 2026 16:55:01 +0300 Subject: [PATCH 009/139] dependencies: update mautrix-go --- go.mod | 6 +++--- go.sum | 12 ++++++------ pkg/connector/wadb/upgrades/upgrades.go | 8 +++----- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 16504a2..a4e7f07 100644 --- a/go.mod +++ b/go.mod @@ -10,15 +10,15 @@ 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.9.11 + go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260716095330-85d99080dee8 + go.mau.fi/whatsmeow v0.0.0-20260718134955-fac667d55293 golang.org/x/image v0.44.0 golang.org/x/net v0.57.0 golang.org/x/sync v0.22.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.29.0 + maunium.net/go/mautrix v0.29.1-0.20260719130752-5743d9b6f27e ) require ( diff --git a/go.sum b/go.sum index 7899436..0786304 100644 --- a/go.sum +++ b/go.sum @@ -71,12 +71,12 @@ github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU= go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0= -go.mau.fi/util v0.9.11 h1:Cus1Lu/t7d3OG6VF4aYWvlUUS0Q4O1/lcpPNJZ0jsw0= -go.mau.fi/util v0.9.11/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= +go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 h1:lsvBEY8MJfYdV61YbwikiQvb0Al/onbmLW5wfl/0tag= +go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= -go.mau.fi/whatsmeow v0.0.0-20260716095330-85d99080dee8 h1:7RQA3v4pCZcmgHaEQXKfHKLVSqPThizkApt6Uw+DcA8= -go.mau.fi/whatsmeow v0.0.0-20260716095330-85d99080dee8/go.mod h1:SX7VdCALDRNx7HZ7mqZxjyi3U7N0QtbSXXXTl8rG5S4= +go.mau.fi/whatsmeow v0.0.0-20260718134955-fac667d55293 h1:tQ2HAlFBB6KidkNqnTHvQ67WLZPNEZbjAQKviAHJr/M= +go.mau.fi/whatsmeow v0.0.0-20260718134955-fac667d55293/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.29.0 h1:OkcBJF1dvp+93EgahxMxOUZZOrGTYculI9IprvRIMOQ= -maunium.net/go/mautrix v0.29.0/go.mod h1:LynuVr8N9nWsE1N4WAE+vItRACDB1pt9M3gN4SIBpeY= +maunium.net/go/mautrix v0.29.1-0.20260719130752-5743d9b6f27e h1:tPGnL/s5dfqhNoVLvmCKY3V60migQsNHBXAhUALBSd8= +maunium.net/go/mautrix v0.29.1-0.20260719130752-5743d9b6f27e/go.mod h1:PeLuIih5jnbwb2xKNNi0Te7AtINEQE2Uv3p6iG3RV80= diff --git a/pkg/connector/wadb/upgrades/upgrades.go b/pkg/connector/wadb/upgrades/upgrades.go index 22c5414..5547982 100644 --- a/pkg/connector/wadb/upgrades/upgrades.go +++ b/pkg/connector/wadb/upgrades/upgrades.go @@ -6,11 +6,9 @@ import ( "go.mau.fi/util/dbutil" ) -var Table dbutil.UpgradeTable - //go:embed *.sql var rawUpgrades embed.FS -func init() { - Table.RegisterFS(rawUpgrades) -} +var Table = dbutil.BuildUpgradeTable(). + WithFS(rawUpgrades). + Finish() From 5affed71d30ad7dea93435458a3d4f51dec385a7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 20 Jul 2026 17:00:59 +0300 Subject: [PATCH 010/139] dependencies: update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a4e7f07..0f147f9 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/tidwall/gjson v1.19.0 go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260718134955-fac667d55293 + go.mau.fi/whatsmeow v0.0.0-20260720135917-a2381054887e golang.org/x/image v0.44.0 golang.org/x/net v0.57.0 golang.org/x/sync v0.22.0 diff --git a/go.sum b/go.sum index 0786304..8138dfe 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 h1:lsvBEY8MJfYdV61YbwikiQvb go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= -go.mau.fi/whatsmeow v0.0.0-20260718134955-fac667d55293 h1:tQ2HAlFBB6KidkNqnTHvQ67WLZPNEZbjAQKviAHJr/M= -go.mau.fi/whatsmeow v0.0.0-20260718134955-fac667d55293/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= +go.mau.fi/whatsmeow v0.0.0-20260720135917-a2381054887e h1:Kn2XQTpYRploXqtmJKjeX4ZYGevhIeQkO2PWGPGdwrY= +go.mau.fi/whatsmeow v0.0.0-20260720135917-a2381054887e/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= From 28b69e99f752c8593ab959e0780f5352145898c4 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 22 Jul 2026 22:27:42 +0300 Subject: [PATCH 011/139] main: remove option to load mclient from plugin --- cmd/mautrix-whatsapp/plugin.go | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 cmd/mautrix-whatsapp/plugin.go diff --git a/cmd/mautrix-whatsapp/plugin.go b/cmd/mautrix-whatsapp/plugin.go deleted file mode 100644 index a1c9ca0..0000000 --- a/cmd/mautrix-whatsapp/plugin.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build amd64 && cgo && !noplugin - -package main - -import ( - "fmt" - "os" - "plugin" - - "go.mau.fi/util/exerrors" - - "go.mau.fi/mautrix-whatsapp/pkg/connector" -) - -func init() { - path := os.Getenv("WM_PLUGIN_PATH") - if path == "" { - return - } - fmt.Println("Loading plugin from", path) - plug := exerrors.Must(plugin.Open(path)) - sym := exerrors.Must(plug.Lookup("NewClient")) - connector.NewMC = sym.(connector.NewMCFunc) -} From 09ec71d21a20666fe4bbacd27e42e530528b76a9 Mon Sep 17 00:00:00 2001 From: Bradley Birch Date: Thu, 23 Jul 2026 14:46:22 +0100 Subject: [PATCH 012/139] capabilities: add more details for polls (#941) --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/capabilities.go | 39 ++++++++++++++++++++--------------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 0f147f9..2aa402f 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/sync v0.22.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.29.1-0.20260719130752-5743d9b6f27e + maunium.net/go/mautrix v0.29.1-0.20260723095015-f7cfa8766d2b ) require ( diff --git a/go.sum b/go.sum index 8138dfe..388c420 100644 --- a/go.sum +++ b/go.sum @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.29.1-0.20260719130752-5743d9b6f27e h1:tPGnL/s5dfqhNoVLvmCKY3V60migQsNHBXAhUALBSd8= -maunium.net/go/mautrix v0.29.1-0.20260719130752-5743d9b6f27e/go.mod h1:PeLuIih5jnbwb2xKNNi0Te7AtINEQE2Uv3p6iG3RV80= +maunium.net/go/mautrix v0.29.1-0.20260723095015-f7cfa8766d2b h1:5ZKdE95/xcAGDUtS7IYhaa/ENdEmedG5CEjkgkzu6H8= +maunium.net/go/mautrix v0.29.1-0.20260723095015-f7cfa8766d2b/go.mod h1:PeLuIih5jnbwb2xKNNi0Te7AtINEQE2Uv3p6iG3RV80= diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index 3e6f658..152e7ad 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -52,7 +52,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit } func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) { - return 1, 8 + return 1, 9 } const WAMaxFileSize = 2000 * 1024 * 1024 @@ -67,7 +67,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel { } func capID() string { - base := "fi.mau.whatsapp.capabilities.2026_05_12" + base := "fi.mau.whatsapp.capabilities.2026_07_22" if ffmpeg.Supported() { return base + "+ffmpeg" } @@ -174,21 +174,26 @@ var whatsappCaps = &event.RoomFeatures{ event.MemberActionKick: event.CapLevelFullySupported, event.MemberActionLeave: event.CapLevelFullySupported, }, - 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, + 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, } var whatsappDMCaps *event.RoomFeatures From 911f5a1568c400b872866323960a9b008d4510e5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 4 Aug 2026 18:10:24 +0300 Subject: [PATCH 013/139] directmedia: use more generic keys for re-requesting media --- pkg/connector/directmedia.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index 89a54fc..8a7afd9 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -52,7 +52,7 @@ func (wa *WhatsAppConnector) SetUseDirectMedia() { } var ErrReloadNeeded = mautrix.RespError{ - ErrCode: "FI.MAU.WHATSAPP_RELOAD_NEEDED", + ErrCode: "COM.BEEPER.MEDIA_RELOAD_NEEDED", Err: "Media is no longer available on WhatsApp servers and must be re-requested from your phone", StatusCode: http.StatusNotFound, } @@ -212,7 +212,7 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse( 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["fi.mau.whatsapp.reload_media"] + val := params["com.beeper.interactive_download_request"] if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") { return nil, ErrReloadNeeded } From 3ec28346c879a682e211d677ede41f75ee06ca3e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 5 Aug 2026 17:08:20 +0300 Subject: [PATCH 014/139] all: switch DMs to use LIDs (#944) --- go.mod | 4 +- go.sum | 8 +- pkg/connector/backfill.go | 60 +++-- pkg/connector/chatinfo.go | 34 ++- pkg/connector/client.go | 16 +- pkg/connector/connector.go | 81 +------ pkg/connector/events.go | 91 ++++---- pkg/connector/handlematrix.go | 22 +- pkg/connector/handlewhatsapp.go | 189 ++++++++-------- pkg/connector/id.go | 23 +- pkg/connector/lidmigrate.go | 212 ++++++++++++++++++ pkg/connector/mediarequest.go | 4 +- pkg/connector/startchat.go | 53 +++-- .../wadb/upgrades/00-latest-schema.sql | 2 +- .../upgrades/08-may-need-lid-dm-deletion.sql | 2 - .../09-may-need-lid-dm-deletion-again.sql | 3 - .../10-history-sync-conversation-to-lid.sql | 8 + pkg/msgconv/from-matrix.go | 2 +- pkg/msgconv/from-whatsapp.go | 41 ++-- pkg/msgconv/wa-poll.go | 44 +--- pkg/waid/id.go | 7 + 21 files changed, 563 insertions(+), 343 deletions(-) create mode 100644 pkg/connector/lidmigrate.go delete mode 100644 pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql delete mode 100644 pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql create mode 100644 pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql diff --git a/go.mod b/go.mod index 2aa402f..9521937 100644 --- a/go.mod +++ b/go.mod @@ -12,13 +12,13 @@ require ( github.com/tidwall/gjson v1.19.0 go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260720135917-a2381054887e + go.mau.fi/whatsmeow v0.0.0-20260805124304-6723e556f35a golang.org/x/image v0.44.0 golang.org/x/net v0.57.0 golang.org/x/sync v0.22.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.29.1-0.20260723095015-f7cfa8766d2b + maunium.net/go/mautrix v0.29.1-0.20260804113340-56938b8a508d ) require ( diff --git a/go.sum b/go.sum index 388c420..322636b 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 h1:lsvBEY8MJfYdV61YbwikiQvb go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= -go.mau.fi/whatsmeow v0.0.0-20260720135917-a2381054887e h1:Kn2XQTpYRploXqtmJKjeX4ZYGevhIeQkO2PWGPGdwrY= -go.mau.fi/whatsmeow v0.0.0-20260720135917-a2381054887e/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= +go.mau.fi/whatsmeow v0.0.0-20260805124304-6723e556f35a h1:TTRND9Dvq7YfT9vwJVQK4Zrit/sm3FHE74B5FWe8CgA= +go.mau.fi/whatsmeow v0.0.0-20260805124304-6723e556f35a/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.29.1-0.20260723095015-f7cfa8766d2b h1:5ZKdE95/xcAGDUtS7IYhaa/ENdEmedG5CEjkgkzu6H8= -maunium.net/go/mautrix v0.29.1-0.20260723095015-f7cfa8766d2b/go.mod h1:PeLuIih5jnbwb2xKNNi0Te7AtINEQE2Uv3p6iG3RV80= +maunium.net/go/mautrix v0.29.1-0.20260804113340-56938b8a508d h1:jbC6tXphot/X1UngnZLv7LcDTzaHBi2phwxrpyhtfrs= +maunium.net/go/mautrix v0.29.1-0.20260804113340-56938b8a508d/go.mod h1:PeLuIih5jnbwb2xKNNi0Te7AtINEQE2Uv3p6iG3RV80= diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index dd04696..65e8f72 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -228,18 +228,18 @@ func (wa *WhatsAppClient) handleWAHistorySync( } else { totalMessageCount += len(conv.GetMessages()) } - if jid.Server == types.HiddenUserServer { - pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid) + if jid.Server == types.DefaultUserServer { + lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) if err != nil { - log.Err(err).Stringer("lid", jid).Msg("Failed to get PN for LID in history sync") - } else if pn.IsEmpty() { - log.Warn().Stringer("lid", jid).Msg("No PN found for LID in history sync") + 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") } else { log.Debug(). - Stringer("lid", jid). - Stringer("pn", pn). - Msg("Rerouting LID DM to phone number in history sync") - jid = pn + Stringer("lid", lid). + Stringer("pn", jid). + Msg("Rerouting phone number DM to LID in history sync") + jid = lid } } log.UpdateContext(func(c zerolog.Context) zerolog.Context { @@ -474,6 +474,9 @@ 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 @@ -528,7 +531,7 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet Forward: params.Forward, }, nil } else if hasMore { - return wa.fetchMessagesFromPhone(ctx, params) + return wa.fetchMessagesFromPhone(ctx, portalJID, params) } return &bridgev2.FetchMessagesResponse{ HasMore: false, @@ -617,6 +620,9 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( continue } } + if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, false) { + return nil, fmt.Errorf("failed to ensure alt JIDs for message %s", evt.Info.ID) + } isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension converted, mediaReq := wa.convertHistorySyncMessage( ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, @@ -651,7 +657,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( }, nil } -func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, params bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) { +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") } @@ -663,9 +669,9 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, params bri msgID := wa.Client.GenerateMessageID() reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{ MessageSource: types.MessageSource{ - Chat: parsed.Chat, + Chat: portalJID, Sender: parsed.Sender, - IsFromMe: parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.Device.GetLID().ToNonAD(), + IsFromMe: wa.IsOwnJID(parsed.Sender), IsGroup: parsed.Chat.Server == types.GroupServer, }, ID: parsed.ID, @@ -673,6 +679,7 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, params bri }, 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") @@ -701,6 +708,20 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID") continue } + if portalJID.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") @@ -749,11 +770,12 @@ 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, nil, isViewOnce, true, nil), - 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)), + 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), Timestamp: info.Timestamp, StreamOrder: info.Timestamp.Unix(), Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)), @@ -762,10 +784,10 @@ func (wa *WhatsAppClient) convertHistorySyncMessage( for _, reaction := range reactions { var sender types.JID if reaction.GetKey().GetFromMe() { - sender = wa.JID + sender = wa.GetLID() } else if reaction.GetKey().GetParticipant() != "" { sender, _ = types.ParseJID(*reaction.Key.Participant) - } else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.BotServer { + } else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BotServer { sender = info.Chat } if sender.IsEmpty() { diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 7ffbb14..1dbebcc 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -99,6 +99,18 @@ 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), } @@ -141,15 +153,20 @@ 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(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)}, + waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)}, + waid.MakeUserID(ownID): {EventSender: wa.makeEventSender(ctx, ownID)}, }, PowerLevels: &bridgev2.PowerLevelOverrides{ Events: map[event.Type]int{ @@ -159,13 +176,14 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge event.StateBeeperDisappearingTimer: 0, }, }, + ExcludeChangesFromTimeline: true, }, - Type: ptr.Ptr(database.RoomTypeDM), + ExcludeChangesFromTimeline: true, } if jid.Server == types.BotServer { info.Topic = ptr.Ptr(BotChatTopic) } - if jid == wa.JID.ToNonAD() { + if wa.IsOwnJID(jid) { // 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)}}, @@ -189,7 +207,7 @@ func (wa *WhatsAppClient) wrapStatusBroadcastInfo(ctx context.Context) *bridgev2 Members: &bridgev2.ChatMemberList{ IsFull: false, MemberMap: map[networkid.UserID]bridgev2.ChatMember{ - waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)}, + waid.MakeUserID(wa.GetLID()): {EventSender: wa.makeEventSender(ctx, wa.GetLID())}, }, }, Type: ptr.Ptr(database.RoomTypeDefault), @@ -316,7 +334,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn } } if info.IsParent && !hasSelf && info.AddressingMode == types.AddressingModeLID { - wrapped.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: wa.makeEventSender(ctx, wa.Device.LID)}) + wrapped.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: wa.makeEventSender(ctx, wa.GetLID())}) } if !info.LinkedParentJID.IsEmpty() { @@ -523,8 +541,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.JID): { - EventSender: wa.makeEventSender(ctx, wa.JID), + waid.MakeUserID(wa.GetLID()): { + EventSender: wa.makeEventSender(ctx, wa.GetLID()), PowerLevel: &ownPowerLevel, }, }, diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 3f19bf7..c5a9348 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -70,6 +70,7 @@ 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() @@ -104,6 +105,7 @@ type WhatsAppClient struct { Client *whatsmeow.Client Device *store.Device JID types.JID + LID types.JID MC mClient historySyncWakeup chan struct{} @@ -185,7 +187,19 @@ func (wa *WhatsAppClient) RegisterPushNotifications(ctx context.Context, pushTyp } func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) bool { - return userID == waid.MakeUserID(wa.JID) + 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 } func (wa *WhatsAppClient) Connect(ctx context.Context) { diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 91b1e11..e262c82 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -28,27 +28,24 @@ 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 { @@ -64,6 +61,8 @@ type WhatsAppConnector struct { mediaEditCache MediaEditCache mediaEditCacheLock sync.RWMutex stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc] + + unmigratedDMs *exsync.Set[networkid.PortalKey] } func init() { @@ -114,6 +113,7 @@ 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,78 +157,7 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error { return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"} } - if !wa.Bridge.Background && wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dms_deleted") == "false" { - wa.deleteLIDDMsMigration(ctx) - } - - return nil -} - -func (wa *WhatsAppConnector) deleteLIDDMsMigration(ctx context.Context) { - log := zerolog.Ctx(ctx).With().Str("action", "delete lid dms").Logger() - portals, err := wa.Bridge.GetAllPortalsWithMXID(ctx) - if err != nil { - log.Err(err).Msg("Failed to get portals for LID DM deletion") - return - } - defer wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_deleted", "true") - if len(portals) == 0 { - log.Debug().Msg("No portals found") - return - } - portalsByKey := make(map[networkid.PortalKey]*bridgev2.Portal, len(portals)) - for _, p := range portals { - if p.Receiver == "" || p.RoomType != database.RoomTypeDM { - continue - } - portalsByKey[p.PortalKey] = p - } - _, err = wa.DB.Exec(ctx, "DELETE FROM whatsapp_history_sync_conversation WHERE chat_jid LIKE '%@lid'") - if err != nil { - log.Err(err).Msg("Failed to remove LID conversations from history sync") - } - for key, portal := range portalsByKey { - parsedID, err := waid.ParsePortalID(key.ID) - if err != nil { - log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to parse portal ID") - continue - } else if parsedID.Server != types.HiddenUserServer { - continue - } - var pnStr string - err = wa.DB.QueryRow(ctx, "SELECT pn FROM whatsmeow_lid_map WHERE lid=$1", parsedID.User).Scan(&pnStr) - if err != nil { - log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to get PN for LID portal") - continue - } - key.ID = waid.MakePortalID(types.JID{User: pnStr, Server: types.DefaultUserServer}) - _, pnPortalExists := portalsByKey[key] - if !pnPortalExists { - log.Warn().Str("portal_id", string(key.ID)).Msg("PN portal does not exist, not deleting LID DM") - continue - } - err = portal.Delete(ctx) - if err != nil { - log.Err(err). - Object("portal_key", portal.PortalKey). - Stringer("portal_mxid", portal.MXID). - Msg("Failed to delete LID DM portal from database") - continue - } - err = wa.Bridge.Bot.DeleteRoom(ctx, portal.MXID, false) - if err != nil { - log.Err(err). - Object("portal_key", portal.PortalKey). - Stringer("portal_mxid", portal.MXID). - Msg("Failed to delete LID DM portal from Matrix") - continue - } - log.Debug(). - Object("portal_key", portal.PortalKey). - Stringer("portal_mxid", portal.MXID). - Msg("Deleted LID DM portal") - } - log.Info().Msg("Finished deleting LID DM portals") + return wa.migrateToLIDDMs(ctx) } func (wa *WhatsAppConnector) Stop() { diff --git a/pkg/connector/events.go b/pkg/connector/events.go index cb51e64..1f4e118 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -39,22 +39,9 @@ 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 { - OrigSource types.MessageSource - Info types.MessageInfo - wa *WhatsAppClient + Info types.MessageInfo + wa *WhatsAppClient } func (evt *MessageInfoWrapper) ShouldCreatePortal() bool { @@ -62,7 +49,26 @@ func (evt *MessageInfoWrapper) ShouldCreatePortal() bool { } func (evt *MessageInfoWrapper) GetPortalKey() networkid.PortalKey { - return evt.wa.getPortalKeyByMessageSource(evt.Info.MessageSource) + 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) } func (evt *MessageInfoWrapper) AddLogContext(c zerolog.Context) zerolog.Context { @@ -73,12 +79,19 @@ 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, evt.Info.Sender) + return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, pickLID(evt.Info.Sender, evt.Info.SenderAlt)) } func (evt *MessageInfoWrapper) GetID() networkid.MessageID { - return waid.MakeMessageID(evt.Info.Chat, evt.Info.Sender, evt.Info.ID) + return waid.MakeMessageIDWithAltSender(evt.Info.Chat, evt.Info.Sender, evt.Info.SenderAlt, evt.Info.ID) } func (evt *MessageInfoWrapper) GetTransactionID() networkid.TransactionID { @@ -135,14 +148,6 @@ 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 } @@ -161,13 +166,6 @@ 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) { @@ -201,7 +199,7 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por ctx = context.WithValue(ctx, msgconv.ContextKeyEditTargetID, evt.Message.GetProtocolMessage().GetKey().GetID()) cm := evt.wa.Main.MsgConv.ToMatrix( - ctx, portal, evt.wa.Client, intent, editedMsg, evt.MsgEvent.RawMessage, &evt.Info, &evt.OrigSource, evt.isViewOnce(), false, previouslyConvertedPart, + ctx, portal, evt.wa.Client, intent, editedMsg, evt.MsgEvent.RawMessage, &evt.Info, evt.isViewOnce(), false, previouslyConvertedPart, ) if evt.isUndecryptableUpsertSubEvent && isFailedMedia(cm) { evt.postHandle = func() { @@ -286,7 +284,7 @@ func (evt *WAMessageEvent) HandleExisting(ctx context.Context, portal *bridgev2. func (evt *WAMessageEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) { evt.wa.EnqueuePortalResync(portal, false) converted := evt.wa.Main.MsgConv.ToMatrix( - ctx, portal, evt.wa.Client, intent, evt.Message, evt.MsgEvent.RawMessage, &evt.Info, &evt.OrigSource, evt.isViewOnce(), false, nil, + ctx, portal, evt.wa.Client, intent, evt.Message, evt.MsgEvent.RawMessage, &evt.Info, evt.isViewOnce(), false, nil, ) if isFailedMedia(converted) { evt.postHandle = func() { @@ -392,7 +390,9 @@ func (evt *WAUndecryptableMessage) GetStreamOrder() int64 { type WAMediaRetry struct { *events.MediaRetry - wa *WhatsAppClient + wa *WhatsAppClient + senderLID types.JID + chatLID types.JID } func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType { @@ -400,7 +400,7 @@ func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType { } func (evt *WAMediaRetry) GetPortalKey() networkid.PortalKey { - return evt.wa.makeWAPortalKey(evt.ChatID) + return evt.wa.makeWAPortalKey(pickLID(evt.ChatID, evt.chatLID)) } func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context { @@ -414,16 +414,23 @@ func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context { func (evt *WAMediaRetry) getRealSender() types.JID { sender := evt.SenderID - 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() + if sender.IsEmpty() { + if evt.FromMe { + if evt.ChatID.Server == types.HiddenUserServer { + sender = evt.wa.GetLID().ToNonAD() + } else { + sender = evt.wa.JID.ToNonAD() + } + } else if evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.HiddenUserServer || evt.ChatID.Server == types.BotServer { + sender = evt.ChatID.ToNonAD() + } } return sender } func (evt *WAMediaRetry) GetSender() bridgev2.EventSender { - return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.getRealSender()) + realSender := pickLID(evt.getRealSender(), evt.senderLID) + return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender) } func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID { diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 9aea18b..c55f523 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -103,11 +103,14 @@ 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.GetStore().GetLID(), req.ID) + wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetLID(), req.ID) msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID)) msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2)) zerolog.Ctx(ctx).Trace().Any("payload", waMsg).Msg("Outgoing message payload") @@ -116,7 +119,7 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg return nil, err } var pickedMessageID networkid.MessageID - if resp.Sender == wa.GetStore().GetLID() && chatJID.Server != types.DefaultUserServer { + if resp.Sender == wa.GetLID() { pickedMessageID = wrappedMsgID2 msg.RemovePending(networkid.TransactionID(wrappedMsgID)) } else { @@ -137,18 +140,17 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg }, nil } -func (wa *WhatsAppClient) PreHandleMatrixReaction(_ context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) { +func (wa *WhatsAppClient) PreHandleMatrixReaction(ctx 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.JID - if portalJID.Server == types.HiddenUserServer || - msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup || - msg.Portal.Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID { - sender = wa.GetStore().GetLID() + 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 } return bridgev2.MatrixReactionPreResponse{ SenderID: waid.MakeUserID(sender), @@ -320,7 +322,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt * if err != nil { continue } - if parsed.Sender.User == wa.GetStore().GetLID().User || parsed.Sender.User == wa.JID.User { + if wa.IsOwnJID(parsed.Sender) { continue } var key types.JID @@ -636,7 +638,7 @@ func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types. lastTS = msgs[0].Timestamp parsed, _ := waid.ParseMessageID(msgs[0].ID) if parsed != nil { - fromMe := parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.GetStore().GetLID().ToNonAD() + fromMe := wa.IsOwnJID(parsed.Sender) var participant *string if chatJID.Server == types.GroupServer { participant = ptr.Ptr(parsed.Sender.String()) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index f12ebad..f8cee2a 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -113,8 +113,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { case *events.HistorySync: wa.UserLogin.Log.Warn().Msg("Unexpected history sync event received") case *events.MediaRetry: - wa.phoneSeen(evt.Timestamp) - success = wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{MediaRetry: evt, wa: wa}).Success + success = wa.handleWAMediaRetry(ctx, evt) case *events.GroupInfo: success = wa.handleWAGroupInfoChange(ctx, evt) @@ -158,7 +157,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.GetStore().GetLID().ToNonAD(), evt.Action.GetName()) + _, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.GetLID().ToNonAD(), evt.Action.GetName()) if err != nil { log.Err(err).Msg("Failed to update push name in store") } @@ -259,65 +258,26 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { return } -func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string, info *types.MessageSource, msgID any) { - if (info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BroadcastServer) && - info.Sender.Server == types.HiddenUserServer && info.SenderAlt.IsEmpty() { - info.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Sender) - } - if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() { - info.RecipientAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat) - } - 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) +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) if err != nil { - 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 + zerolog.Ctx(ctx).Err(err).Stringer("sender", info.Sender).Msg("Failed to get LID for sender") + return false } } + 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) { @@ -325,6 +285,9 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast { return } + if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { + return false + } parsedMessageType := getMessageType(evt.Message) if encReact := evt.Message.GetEncReactionMessage(); encReact != nil { decrypted, err := wa.Client.DecryptReaction(ctx, evt) @@ -358,8 +321,6 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa evt.UnwrapRaw() parsedMessageType = getMessageType(evt.Message) } - origSource := evt.Info.MessageSource - wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID) wa.UserLogin.Log.Trace(). Any("info", evt.Info). Any("payload", evt.Message). @@ -410,9 +371,8 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{ MessageInfoWrapper: &MessageInfoWrapper{ - OrigSource: origSource, - Info: evt.Info, - wa: wa, + Info: evt.Info, + wa: wa, }, Message: evt.Message, MsgEvent: evt, @@ -424,7 +384,9 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa } func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool { - wa.rerouteWAMessage(ctx, "undecryptable message", &evt.Info.MessageSource, evt.Info.ID) + if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { + return false + } wa.UserLogin.Log.Debug(). Any("info", evt.Info). Bool("unavailable", evt.IsUnavailable). @@ -447,12 +409,44 @@ 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: @@ -464,28 +458,34 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei default: return true } - targets := make([]networkid.MessageID, len(evt.MessageIDs)) - messageSender := wa.JID + targets := make([]networkid.MessageID, 0, len(evt.MessageIDs)) + messageSender := wa.GetLID() if !evt.MessageSender.IsEmpty() { messageSender = evt.MessageSender - // Second part of rerouting receipts in LID chats - if messageSender == origChat && evt.Chat != origChat { - messageSender = evt.Chat - } - } else if evt.Chat.Server == types.GroupServer && evt.Sender.Server == types.HiddenUserServer { - lid := wa.GetStore().GetLID() - if !lid.IsEmpty() { - messageSender = lid + } + 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 } } - for i, id := range evt.MessageIDs { - targets[i] = waid.MakeMessageID(evt.Chat, messageSender, id) + 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 } res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{ EventMeta: simplevent.EventMeta{ Type: evtType, PortalKey: wa.makeWAPortalKey(evt.Chat), - Sender: wa.makeEventSender(ctx, evt.Sender), + Sender: wa.makeEventSender(ctx, senderLID), Timestamp: evt.Timestamp, }, Targets: targets, @@ -494,11 +494,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.HiddenUserServer && evt.Sender.ToNonAD() == evt.Chat { + if evt.Chat.Server == types.DefaultUserServer && evt.Sender.ToNonAD() == evt.Chat { if evt.SenderAlt.IsEmpty() { - evt.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, evt.Sender) + evt.SenderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Sender) } - if evt.SenderAlt.Server == types.DefaultUserServer { + if evt.SenderAlt.Server == types.HiddenUserServer { evt.Sender, evt.SenderAlt = evt.SenderAlt, evt.Sender evt.Chat = evt.Sender.ToNonAD() } @@ -535,6 +535,7 @@ 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, @@ -548,12 +549,15 @@ func (wa *WhatsAppClient) handleWACallStart(ctx context.Context, group, sender, if !wa.Main.Config.CallStartNotices || time.Since(ts) > callEventMaxAge { return true } - if sender.Server == types.HiddenUserServer && senderAlt.Server == types.DefaultUserServer { + if sender.Server == types.DefaultUserServer && senderAlt.IsEmpty() { + senderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, sender) + } + if sender.Server == types.DefaultUserServer && senderAlt.Server == types.HiddenUserServer { wa.UserLogin.Log.Debug(). - Stringer("lid", sender). - Stringer("pn", senderAlt). + Stringer("lid", senderAlt). + Stringer("pn", sender). Str("call_id", id). - Msg("Forced LID caller to phone number in incoming call") + Msg("Forced phone number caller to LID in incoming call") sender, senderAlt = senderAlt, sender } chat := group @@ -600,6 +604,12 @@ 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, @@ -650,13 +660,14 @@ 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, evt.SenderJID, evt.MessageID), + TargetMessage: waid.MakeMessageID(chatJID, senderJID, evt.MessageID), OnlyForMe: true, }).Success } @@ -667,7 +678,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.JID), + Sender: wa.makeEventSender(ctx, wa.GetLID()), Timestamp: evt.Timestamp, }, ReadUpTo: evt.Timestamp, diff --git a/pkg/connector/id.go b/pkg/connector/id.go index c07e431..c0aa06b 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: id.User == wa.GetStore().GetJID().User || id.User == wa.GetStore().GetLID().User, + IsFromMe: wa.IsOwnJID(id), Sender: waid.MakeUserID(id), SenderLogin: waid.MakeUserLoginID(senderLoginJID), } @@ -60,24 +60,25 @@ func (wa *WhatsAppClient) messageIDToKey(id *waid.ParsedMessageID) *waCommon.Mes RemoteJID: ptr.Ptr(id.Chat.String()), ID: ptr.Ptr(id.ID), } - if id.Sender.User == wa.GetStore().GetJID().User || id.Sender.User == wa.GetStore().GetLID().User { + if wa.IsOwnJID(id.Sender) { 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, chatJID types.JID) types.JID { - if chatJID.Server == types.HiddenUserServer { - if pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, chatJID); err != nil { +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 { wa.UserLogin.Log.Err(err). - Stringer("lid", chatJID). - Msg("Failed to get phone number for LID chat") - } else if !pn.IsEmpty() { - return pn.ToNonAD() + Stringer("pn", jid). + Msg("Failed to get LID for phone number chat") + } else if !lidForPN.IsEmpty() { + return lidForPN } } - return chatJID + return jid } diff --git a/pkg/connector/lidmigrate.go b/pkg/connector/lidmigrate.go new file mode 100644 index 0000000..cfab099 --- /dev/null +++ b/pkg/connector/lidmigrate.go @@ -0,0 +1,212 @@ +// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2026 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package connector + +import ( + "context" + "fmt" + "time" + + "github.com/rs/zerolog" + "go.mau.fi/util/dbutil" + "go.mau.fi/whatsmeow/types" + + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/networkid" + "maunium.net/go/mautrix/event" + + "go.mau.fi/mautrix-whatsapp/pkg/waid" +) + +func (wa *WhatsAppClient) FindAltTargetMessage(ctx context.Context, targetMsg networkid.MessageID, evt bridgev2.RemoteEventWithTargetMessage) (alts []networkid.MessageID, err error) { + parsed, err := waid.ParseMessageID(targetMsg) + if err != nil { + return nil, fmt.Errorf("failed to parse target message ID: %w", err) + } + altSender, err := wa.GetStore().GetAltJID(ctx, parsed.Sender) + if err != nil { + return nil, err + } + var altChat types.JID + if parsed.Chat.Server == types.HiddenUserServer { + altChat, err = wa.GetStore().LIDs.GetPNForLID(ctx, parsed.Chat) + if err != nil { + return nil, err + } + } + if !altSender.IsEmpty() { + altSenderID := *parsed + altSenderID.Sender = altSender + alts = append(alts, altSenderID.String()) + } + if !altChat.IsEmpty() { + altChatID := *parsed + altChatID.Chat = altChat + if altSender.Server == types.DefaultUserServer { + altChatID.Sender = altSender + } + alts = append(alts, altChatID.String()) + } + return +} + +func (wa *WhatsAppClient) checkAllPhonesInMessage(ctx context.Context, info *types.MessageSource) (ok bool) { + for _, jid := range []types.JID{info.Sender, info.SenderAlt, info.Chat, info.RecipientAlt, info.BroadcastListOwner} { + if !wa.reIDPhoneDMToLIDIfNeeded(ctx, jid) { + return false + } + } + return true +} + +func (wa *WhatsAppClient) reIDPhoneDMToLIDIfNeeded(ctx context.Context, pn types.JID) (ok bool) { + if pn.Server != types.DefaultUserServer { + return true + } + portalKey := wa.makeWAPortalKey(pn) + if wa.Main.unmigratedDMs.Has(portalKey) { + lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, pn) + if err != nil { + zerolog.Ctx(ctx).Err(err).Stringer("pn", pn).Msg("Failed to get LID for PN") + return false + } else if lid.IsEmpty() { + zerolog.Ctx(ctx).Warn().Stringer("pn", pn).Msg("No found LID for phone number") + return true + } + zerolog.Ctx(ctx).Info(). + Object("portal_key", portalKey). + Stringer("pn", pn). + Stringer("lid", lid). + Msg("Received event for portal in unmigrated DMs list, trying migration") + _, err = wa.Main.reIDPhoneDMToLID(ctx, pn, lid, wa.UserLogin.ID) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to re-ID phone DM to LID") + return false + } + } + return true +} + +func (wa *WhatsAppConnector) reIDPhoneDMToLID(ctx context.Context, pn, lid types.JID, receiver networkid.UserLoginID) (bridgev2.ReIDResult, error) { + pnKey := networkid.PortalKey{ + ID: waid.MakePortalID(pn), + Receiver: receiver, + } + lidKey := networkid.PortalKey{ + ID: waid.MakePortalID(lid), + Receiver: receiver, + } + result, portal, err := wa.Bridge.ReIDPortal(ctx, pnKey, lidKey) + if err != nil { + return result, err + } + if result == bridgev2.ReIDResultSourceReIDd || result == bridgev2.ReIDResultTargetDeletedAndSourceReIDd { + var pnGhost, lidGhost *bridgev2.Ghost + pnGhost, err = wa.Bridge.GetGhostByID(ctx, waid.MakeUserID(pn)) + if err != nil { + return result, fmt.Errorf("failed to get PN ghost: %w", err) + } + lidGhost, err = wa.Bridge.GetGhostByID(ctx, waid.MakeUserID(lid)) + if err != nil { + return result, fmt.Errorf("failed to get LID ghost: %w", err) + } + _, err = pnGhost.Intent.SendState(ctx, portal.MXID, event.StateMember, pnGhost.Intent.GetMXID().String(), &event.Content{ + Parsed: &event.MemberEventContent{Membership: event.MembershipLeave, Reason: "Migrating to LIDs"}, + Raw: map[string]any{"com.beeper.exclude_from_timeline": true}, + }, time.Time{}) + if err != nil { + return result, fmt.Errorf("failed to send leave event for PN ghost: %w", err) + } + _, err = wa.Bridge.Bot.SendState(ctx, portal.MXID, event.StateMember, lidGhost.Intent.GetMXID().String(), &event.Content{ + Parsed: &event.MemberEventContent{Membership: event.MembershipInvite, Reason: "Migrating to LIDs"}, + Raw: map[string]any{"com.beeper.exclude_from_timeline": true}, + }, time.Time{}) + if err != nil { + return result, fmt.Errorf("failed to send invite event for LID ghost: %w", err) + } + _, err = lidGhost.Intent.SendState(ctx, portal.MXID, event.StateMember, lidGhost.Intent.GetMXID().String(), &event.Content{ + Parsed: &event.MemberEventContent{Membership: event.MembershipJoin, Reason: "Migrating to LIDs"}, + Raw: map[string]any{"com.beeper.exclude_from_timeline": true}, + }, time.Time{}) + if err != nil { + return result, fmt.Errorf("failed to send join event for LID ghost: %w", err) + } + portal.OtherUserID = lidGhost.ID + err = portal.Save(ctx) + if err != nil { + return result, fmt.Errorf("failed to save portal after re-ID: %w", err) + } + portal.UpdateBridgeInfo(ctx) + } + return result, nil +} + +var scanPortalKey = dbutil.ConvertRowFn[networkid.PortalKey](func(row dbutil.Scannable) (key networkid.PortalKey, err error) { + err = row.Scan(&key.ID, &key.Receiver) + return +}) + +func (wa *WhatsAppConnector) migrateToLIDDMs(ctx context.Context) error { + if wa.Bridge.Background { + if wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dms_migrated") == "true" { + return nil + } + return fmt.Errorf("can't migrate to LID DMs in background mode") + } + log := zerolog.Ctx(ctx).With().Str("action", "migrate to lid dms").Logger() + const findPNPortals = "SELECT id, receiver FROM portal WHERE bridge_id=$1 AND room_type='dm' AND id LIKE '%@s.whatsapp.net'" + pnPortalKeys, err := scanPortalKey.NewRowIter(wa.Bridge.DB.Query(ctx, findPNPortals, wa.Bridge.ID)).AsList() + if err != nil { + return fmt.Errorf("failed to get phone number portals: %w", err) + } + var updatedPortals, missingLID int + for _, key := range pnPortalKeys { + pnJID, 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 pnJID.Server != types.DefaultUserServer { + continue + } + lid, err := wa.DeviceStore.LIDMap.GetLIDForPN(ctx, pnJID) + if err != nil { + return fmt.Errorf("failed to get LID for PN portal %s: %w", key.ID, err) + } else if lid.IsEmpty() { + log.Warn().Stringer("pn", pnJID).Msg("No LID for PN portal") + wa.unmigratedDMs.Add(key) + missingLID++ + continue + } + res, err := wa.reIDPhoneDMToLID(ctx, pnJID, lid, key.Receiver) + if err != nil { + return fmt.Errorf("failed to re-ID %s to %s: %w", pnJID, lid, err) + } + updatedPortals++ + log.Info(). + Stringer("pn", pnJID). + Stringer("lid", lid). + Stringer("result", res). + Msg("Re-ID'd phone number DM portal") + } + log.Info(). + Int("updated_portals", updatedPortals). + Int("total_pn_portals", len(pnPortalKeys)). + Int("missing_lid", missingLID). + Msg("Finished re-IDing phone number DM portals") + wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_migrated", "true") + return nil +} diff --git a/pkg/connector/mediarequest.go b/pkg/connector/mediarequest.go index 2686250..3806196 100644 --- a/pkg/connector/mediarequest.go +++ b/pkg/connector/mediarequest.go @@ -155,8 +155,8 @@ func (wa *WhatsAppClient) sendMediaRequestDirect(ctx context.Context, rawMsgID n return wa.Client.SendMediaRetryReceipt(ctx, &types.MessageInfo{ ID: msgID.ID, MessageSource: types.MessageSource{ - IsFromMe: msgID.Sender.User == wa.JID.User, - IsGroup: msgID.Chat.Server != types.DefaultUserServer && msgID.Chat.Server != types.BotServer, + IsFromMe: wa.IsOwnJID(msgID.Sender), + IsGroup: msgID.Chat.Server != types.DefaultUserServer && msgID.Chat.Server != types.HiddenUserServer && msgID.Chat.Server != types.BotServer, Sender: msgID.Sender, Chat: msgID.Chat, }, diff --git a/pkg/connector/startchat.go b/pkg/connector/startchat.go index fb2ce16..f0d869b 100644 --- a/pkg/connector/startchat.go +++ b/pkg/connector/startchat.go @@ -25,6 +25,7 @@ import ( "time" "github.com/rs/zerolog" + "go.mau.fi/util/exmaps" "go.mau.fi/util/exsync" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" @@ -119,16 +120,24 @@ func (wa *WhatsAppConnector) ValidateUserID(id networkid.UserID) bool { } } -func (wa *WhatsAppClient) startChatLIDToPN(ctx context.Context, jid types.JID) (types.JID, error) { - if jid.Server == types.HiddenUserServer { - pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid) +func (wa *WhatsAppClient) startChatPNToLID(ctx context.Context, jid types.JID) (types.JID, error) { + if jid.Server == types.DefaultUserServer { + lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) if err != nil { - return jid, fmt.Errorf("failed to get phone number for lid: %w", err) - } else if pn.IsEmpty() { - // Don't allow starting chats with LIDs for now - return jid, fmt.Errorf("phone number not found") + return jid, fmt.Errorf("failed to get lid for phone number: %w", err) + } else if lid.IsEmpty() { + resp, err := wa.Client.GetUserInfo(ctx, []types.JID{jid}) + if err != nil { + return jid, fmt.Errorf("failed to get user info for phone number: %w", err) + } else if info, ok := resp[jid]; !ok { + return jid, fmt.Errorf("server didn't return user info for phone number") + } else if info.LID.IsEmpty() { + return jid, fmt.Errorf("server didn't return lid for phone number") + } else { + return info.LID, nil + } } - return pn, nil + return lid, nil } return jid, nil } @@ -147,7 +156,7 @@ func (wa *WhatsAppClient) makeCreateChatResponse(ctx context.Context, jid, origJ func (wa *WhatsAppClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, error) { origJID := waid.ParseUserID(ghost.ID) - jid, err := wa.startChatLIDToPN(ctx, origJID) + jid, err := wa.startChatPNToLID(ctx, origJID) if err != nil { return nil, err } @@ -159,7 +168,7 @@ func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier stri if err != nil { return nil, err } - jid, err := wa.startChatLIDToPN(ctx, origJID) + jid, err := wa.startChatPNToLID(ctx, origJID) if err != nil { return nil, err } @@ -199,6 +208,7 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl return nil, err } resp := make([]*bridgev2.ResolveIdentifierResponse, 0, len(contacts)) + addedIDs := make(exmaps.Set[types.JID]) for jid, contactInfo := range contacts { if onlyContacts && (contactInfo.FirstName == "" && contactInfo.FullName == "") { continue @@ -206,12 +216,30 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl if !matchesQuery(contactInfo.PushName, filter) && !matchesQuery(contactInfo.FullName, filter) && !matchesQuery(jid.User, filter) { continue } + var lid types.JID + if jid.Server == types.HiddenUserServer { + lid = jid + } else if jid.Server == types.DefaultUserServer { + lid, err = wa.GetStore().LIDs.GetLIDForPN(ctx, jid) + if err != nil { + return nil, fmt.Errorf("failed to get lid for phone number %s: %w", jid, err) + } else if !lid.IsEmpty() { + jid = lid + } + } + if !addedIDs.Add(jid) { + continue + } + var chatResp *bridgev2.CreateChatResponse + if !lid.IsEmpty() { + chatResp = &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(lid)} + } ghost, _ := wa.Main.Bridge.GetGhostByID(ctx, waid.MakeUserID(jid)) resp = append(resp, &bridgev2.ResolveIdentifierResponse{ Ghost: ghost, UserID: waid.MakeUserID(jid), UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, false), - Chat: &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(jid)}, + Chat: chatResp, }) } return resp, nil @@ -229,8 +257,7 @@ func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.Grou } for i, participant := range params.Participants { jid := waid.ParseUserID(participant) - // Normalize to PN if it's a LID - jid, err := wa.startChatLIDToPN(ctx, jid) + jid, err := wa.startChatPNToLID(ctx, jid) if err != nil { return nil, fmt.Errorf("failed to normalize participant %s: %w", participant, err) } diff --git a/pkg/connector/wadb/upgrades/00-latest-schema.sql b/pkg/connector/wadb/upgrades/00-latest-schema.sql index 850f5b4..0f1fef9 100644 --- a/pkg/connector/wadb/upgrades/00-latest-schema.sql +++ b/pkg/connector/wadb/upgrades/00-latest-schema.sql @@ -1,4 +1,4 @@ --- v0 -> v9 (compatible with v3+): Latest revision +-- v0 -> v10 (compatible with v3+): Latest revision CREATE TABLE whatsapp_poll_option_id ( bridge_id TEXT NOT NULL, diff --git a/pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql b/pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql deleted file mode 100644 index d5b4ca0..0000000 --- a/pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql +++ /dev/null @@ -1,2 +0,0 @@ --- v8 (compatible with v3+): Mark LID DMs for deletion -INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false'); diff --git a/pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql b/pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql deleted file mode 100644 index 5c32e65..0000000 --- a/pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql +++ /dev/null @@ -1,3 +0,0 @@ --- v9 (compatible with v3+): Mark LID DMs for deletion (again) -DELETE FROM kv_store WHERE bridge_id='' AND key='whatsapp_lid_dms_deleted'; -INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false'); diff --git a/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql b/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql new file mode 100644 index 0000000..9e50f51 --- /dev/null +++ b/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql @@ -0,0 +1,8 @@ +-- v10 (compatible with v3+): Move history sync conversations to LIDs +UPDATE whatsapp_history_sync_conversation +SET chat_jid=COALESCE( + (SELECT lid || '@lid' FROM whatsmeow_lid_map WHERE pn=replace(chat_jid, '@s.whatsapp.net', '')), + chat_jid +) +WHERE chat_jid LIKE '%@s.whatsapp.net'; +DELETE FROM portal WHERE id LIKE '%@s.whatsapp.net' AND (mxid IS NULL OR mxid='') AND room_type=''; diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index 49aedf8..57f9113 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -150,7 +150,7 @@ func (mc *MessageConverter) ToWhatsApp( } lid := parsedID.Sender if lid.Server == types.DefaultUserServer { - lid, err = client.Store.LIDs.GetLIDForPN(ctx, parsedID.Sender) + lid, err = client.Store.LIDs.GetLIDForPN(ctx, lid) if err != nil { return nil, nil, fmt.Errorf("failed to get LID for PN: %w", err) } diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go index e8cb032..e912732 100644 --- a/pkg/msgconv/from-whatsapp.go +++ b/pkg/msgconv/from-whatsapp.go @@ -136,7 +136,6 @@ func (mc *MessageConverter) ToMatrix( waMsg *waE2E.Message, rawWaMsg *waE2E.Message, info *types.MessageInfo, - origSource *types.MessageSource, isViewOnce bool, isBackfill bool, previouslyConvertedPart *bridgev2.ConvertedMessagePart, @@ -183,7 +182,7 @@ func (mc *MessageConverter) ToMatrix( case waMsg.PollCreationMessageV6 != nil: part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV6) case waMsg.PollUpdateMessage != nil: - part, contextInfo = mc.convertPollUpdateMessage(ctx, info, origSource, waMsg.PollUpdateMessage) + part, contextInfo = mc.convertPollUpdateMessage(ctx, info, waMsg.PollUpdateMessage) case waMsg.EventMessage != nil: part, contextInfo = mc.convertEventMessage(ctx, waMsg.EventMessage) case waMsg.PinInChatMessage != nil: @@ -272,30 +271,26 @@ func (mc *MessageConverter) ToMatrix( if chat.IsEmpty() { chat, _ = waid.ParsePortalID(portal.ID) } - // We reroute all DMs to the phone number JID, so reroute reply participants too - pcp = rerouteMessageKey(ctx, chat, pcp, getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID) - if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && pcp.Server == types.HiddenUserServer { - pcpPN, _ := store.LIDs.GetPNForLID(ctx, pcp) - zerolog.Ctx(ctx).Debug(). - Stringer("orig_participant", pcp). - Stringer("rerouted_participant", pcpPN). - Msg("Rerouting reply target (PN recipient in LID DM)") - if !pcpPN.IsEmpty() { - pcp = pcpPN - } - } else if store != nil && chat.Server == types.GroupServer && pcp.Server == types.DefaultUserServer && getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID { - pcpLID, _ := store.LIDs.GetLIDForPN(ctx, pcp) - zerolog.Ctx(ctx).Debug(). - Stringer("orig_participant", pcp). - Stringer("rerouted_participant", pcpLID). - Msg("Rerouting reply target (PN recipient in LID group)") - if !pcpLID.IsEmpty() { - pcp = pcpLID - } - } cm.ReplyTo = &networkid.MessageOptionalPartID{ MessageID: waid.MakeMessageID(chat, pcp, contextInfo.GetStanzaID()), } + var pn, lid types.JID + if pcp.Server == types.DefaultUserServer { + pn = pcp + lid, _ = client.Store.LIDs.GetLIDForPN(ctx, pcp) + } else if pcp.Server == types.HiddenUserServer { + lid = pcp + pn, _ = client.Store.LIDs.GetPNForLID(ctx, pcp) + } else if pcp.Server == types.BotServer { + lid = pcp + } + if !pn.IsEmpty() { + cm.ReplyToLogin = waid.MakeUserLoginID(pn) + } + if !lid.IsEmpty() { + cm.ReplyToUser = waid.MakeUserID(lid) + } + // TODO set reply to room } if contextInfo.GetIsForwarded() { hasCaption := part.Content.FileName != "" && part.Content.FileName != part.Content.Body diff --git a/pkg/msgconv/wa-poll.go b/pkg/msgconv/wa-poll.go index dfe1a25..881a5fb 100644 --- a/pkg/msgconv/wa-poll.go +++ b/pkg/msgconv/wa-poll.go @@ -94,31 +94,7 @@ func (mc *MessageConverter) convertPollCreationMessage(ctx context.Context, msg }, msg.GetContextInfo() } -func rerouteMessageKey(ctx context.Context, chat, sender types.JID, groupLIDAddressing bool) types.JID { - if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && sender.Server == types.HiddenUserServer { - senderPN, _ := store.LIDs.GetPNForLID(ctx, sender) - zerolog.Ctx(ctx).Debug(). - Stringer("orig_participant", sender). - Stringer("rerouted_participant", senderPN). - Msg("Rerouting message key (PN recipient in LID DM)") - if !senderPN.IsEmpty() { - return senderPN - } - } else if store != nil && chat.Server == types.GroupServer && sender.Server == types.DefaultUserServer && groupLIDAddressing { - senderLID, _ := store.LIDs.GetLIDForPN(ctx, sender) - zerolog.Ctx(ctx).Debug(). - Stringer("orig_participant", sender). - Stringer("rerouted_participant", senderLID). - Msg("Rerouting message key (PN recipient in LID group)") - if !senderLID.IsEmpty() { - return senderLID - } - } - return sender -} - func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender types.JID, key *waCommon.MessageKey) networkid.MessageID { - groupLIDAddressing := sender.Server == types.HiddenUserServer sender = sender.ToNonAD() var err error if !key.GetFromMe() { @@ -131,7 +107,7 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender if sender.Server == types.LegacyUserServer { sender.Server = types.DefaultUserServer } - } else if chat.Server == types.DefaultUserServer || chat.Server == types.BotServer { + } else if chat.Server == types.DefaultUserServer || chat.Server == types.HiddenUserServer || chat.Server == types.BotServer { if sender.User == client.Store.GetJID().User || sender.User == client.Store.GetLID().User { // Message key is not from the sender, but message sender (containing key) is me, // so message key sender is the other user in the DM @@ -139,7 +115,11 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender } else { // Message key is not from the sender, but message sender (containing key) is not me, // so message key sender is me - sender = client.Store.GetJID().ToNonAD() + if chat.Server == types.HiddenUserServer { + sender = client.Store.GetLID().ToNonAD() + } else { + sender = client.Store.GetJID().ToNonAD() + } } } else { zerolog.Ctx(ctx).Warn(). @@ -157,10 +137,6 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender chat = remoteJID } } - sender = rerouteMessageKey( - context.WithValue(ctx, contextKeyClient, client), - chat, sender, groupLIDAddressing, - ) return waid.MakeMessageID(chat, sender, key.GetID()) } @@ -170,7 +146,7 @@ var failedPollUpdatePart = &bridgev2.ConvertedMessagePart{ DontBridge: true, } -func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *types.MessageInfo, origSource *types.MessageSource, msg *waE2E.PollUpdateMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { +func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *types.MessageInfo, msg *waE2E.PollUpdateMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { log := zerolog.Ctx(ctx) pollMessageID := KeyToMessageID(ctx, getClient(ctx), info.Chat, info.Sender, msg.PollCreationMessageKey) pollMessage, err := mc.Bridge.DB.Message.GetPartByID(ctx, getPortal(ctx).Receiver, pollMessageID, "") @@ -181,12 +157,8 @@ func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info * log.Warn().Str("target_message_id", string(pollMessageID)).Msg("Poll update target message not found") return failedPollUpdatePart, nil } - infoForDecrypt := *info - if origSource != nil { - infoForDecrypt.MessageSource = *origSource - } vote, err := getClient(ctx).DecryptPollVote(ctx, &events.Message{ - Info: infoForDecrypt, + Info: *info, Message: &waE2E.Message{PollUpdateMessage: msg}, }) if err != nil { diff --git a/pkg/waid/id.go b/pkg/waid/id.go index e522f5f..7f8fe5b 100644 --- a/pkg/waid/id.go +++ b/pkg/waid/id.go @@ -83,6 +83,13 @@ func MakeMessageID(chat, sender types.JID, id types.MessageID) networkid.Message return networkid.MessageID(fmt.Sprintf("%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), id)) } +func MakeMessageIDWithAltSender(chat, sender, altSender types.JID, id types.MessageID) networkid.MessageID { + if chat.Server == types.HiddenUserServer && sender.Server == types.DefaultUserServer && altSender.Server == types.HiddenUserServer { + sender = altSender + } + return MakeMessageID(chat, sender, id) +} + func MakeFakeMessageID(chat, sender types.JID, data string) networkid.MessageID { return networkid.MessageID(fmt.Sprintf("fake:%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), data)) } From ee8592c842ebdd4423d28034e7242df62f8a7aba Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 5 Aug 2026 17:18:31 +0300 Subject: [PATCH 015/139] wadb: make lid migration of history sync conversations safer --- .../10-history-sync-conversation-to-lid.sql | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql b/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql index 9e50f51..334c597 100644 --- a/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql +++ b/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql @@ -1,8 +1,22 @@ -- v10 (compatible with v3+): Move history sync conversations to LIDs + +-- Delete history sync conversations where a @lid conversation already exists +DELETE FROM whatsapp_history_sync_conversation +WHERE chat_jid LIKE '%@lid' AND EXISTS ( + SELECT 1 + FROM whatsapp_history_sync_conversation pnconv + WHERE pnconv.chat_jid=( + SELECT pn || '@s.whatsapp.net' + FROM whatsmeow_lid_map + WHERE lid=replace(whatsapp_history_sync_conversation.chat_jid, '@lid', '') + ) +); + +-- Update all phone number conversations to lids if the lid is known UPDATE whatsapp_history_sync_conversation -SET chat_jid=COALESCE( - (SELECT lid || '@lid' FROM whatsmeow_lid_map WHERE pn=replace(chat_jid, '@s.whatsapp.net', '')), - chat_jid -) -WHERE chat_jid LIKE '%@s.whatsapp.net'; +SET chat_jid=(SELECT lid || '@lid' FROM whatsmeow_lid_map WHERE pn=replace(chat_jid, '@s.whatsapp.net', '')) +WHERE chat_jid LIKE '%@s.whatsapp.net' + AND EXISTS (SELECT 1 FROM whatsmeow_lid_map WHERE pn=replace(chat_jid, '@s.whatsapp.net', '')); + +-- Delete blank phone number portals DELETE FROM portal WHERE id LIKE '%@s.whatsapp.net' AND (mxid IS NULL OR mxid='') AND room_type=''; From 09a43f5099c7441b69642b8c391de4fc965e6c12 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 6 Aug 2026 15:03:21 +0300 Subject: [PATCH 016/139] handlewhatsapp: add analytics for app state errors --- pkg/connector/handlewhatsapp.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index f8cee2a..fce47df 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -880,7 +880,12 @@ 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) @@ -937,6 +942,9 @@ 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 { From 85ace3ddcb011fc249b25f80893aa9a0090001cd Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 6 Aug 2026 18:06:54 +0300 Subject: [PATCH 017/139] dependencies: update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9521937..9aa9e7e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/tidwall/gjson v1.19.0 go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260805124304-6723e556f35a + go.mau.fi/whatsmeow v0.0.0-20260806130833-ad9cc5cbdf9d golang.org/x/image v0.44.0 golang.org/x/net v0.57.0 golang.org/x/sync v0.22.0 diff --git a/go.sum b/go.sum index 322636b..9ba5037 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 h1:lsvBEY8MJfYdV61YbwikiQvb go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= -go.mau.fi/whatsmeow v0.0.0-20260805124304-6723e556f35a h1:TTRND9Dvq7YfT9vwJVQK4Zrit/sm3FHE74B5FWe8CgA= -go.mau.fi/whatsmeow v0.0.0-20260805124304-6723e556f35a/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= +go.mau.fi/whatsmeow v0.0.0-20260806130833-ad9cc5cbdf9d h1:fr7vlv8+xPmAgClPXaXLFspjW2LtXY88CRZ9MB3fZe8= +go.mau.fi/whatsmeow v0.0.0-20260806130833-ad9cc5cbdf9d/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= From d96c57046c0158414bd7b208f176e2447b1dd9aa Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 6 Aug 2026 22:23:50 +0300 Subject: [PATCH 018/139] dependencies: update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9aa9e7e..18468ba 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/tidwall/gjson v1.19.0 go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260806130833-ad9cc5cbdf9d + go.mau.fi/whatsmeow v0.0.0-20260806192107-39b719baa629 golang.org/x/image v0.44.0 golang.org/x/net v0.57.0 golang.org/x/sync v0.22.0 diff --git a/go.sum b/go.sum index 9ba5037..b543aa6 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 h1:lsvBEY8MJfYdV61YbwikiQvb go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= -go.mau.fi/whatsmeow v0.0.0-20260806130833-ad9cc5cbdf9d h1:fr7vlv8+xPmAgClPXaXLFspjW2LtXY88CRZ9MB3fZe8= -go.mau.fi/whatsmeow v0.0.0-20260806130833-ad9cc5cbdf9d/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= +go.mau.fi/whatsmeow v0.0.0-20260806192107-39b719baa629 h1:q3Jfm1wTs48+b0yXYy3TVlIGi4OR6FvJByAppvYdHTY= +go.mau.fi/whatsmeow v0.0.0-20260806192107-39b719baa629/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= From 3a31b5bd8f1ae2684bf6d34a110c69b1c144308d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 7 Aug 2026 12:21:17 +0300 Subject: [PATCH 019/139] dependencies: update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 18468ba..2b0f16e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/tidwall/gjson v1.19.0 go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260806192107-39b719baa629 + go.mau.fi/whatsmeow v0.0.0-20260806224404-e277b766ab33 golang.org/x/image v0.44.0 golang.org/x/net v0.57.0 golang.org/x/sync v0.22.0 diff --git a/go.sum b/go.sum index b543aa6..d01face 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 h1:lsvBEY8MJfYdV61YbwikiQvb go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= -go.mau.fi/whatsmeow v0.0.0-20260806192107-39b719baa629 h1:q3Jfm1wTs48+b0yXYy3TVlIGi4OR6FvJByAppvYdHTY= -go.mau.fi/whatsmeow v0.0.0-20260806192107-39b719baa629/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= +go.mau.fi/whatsmeow v0.0.0-20260806224404-e277b766ab33 h1:NTzJLmLB8ysR35TXPT8Dl4h2AHumaV4sKDX6JeJuVA4= +go.mau.fi/whatsmeow v0.0.0-20260806224404-e277b766ab33/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= From b0be90d0d10c91bb1c96525995167b45abb6b1e6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 10 Aug 2026 14:22:47 +0300 Subject: [PATCH 020/139] backfill: add hack for duplicate phone number messages --- pkg/connector/backfill.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 65e8f72..7356d11 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -12,6 +12,7 @@ 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" @@ -597,6 +598,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( newestTS := messages[0].GetMessageTimestamp() convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages)) var mediaRequests []*wadb.MediaRequest + dups := make(exmaps.Set[networkid.MessageID]) for i, msg := range messages { evt, err := wa.Client.ParseWebMessage(portalJID, msg) if err != nil { @@ -627,6 +629,11 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( converted, mediaReq := wa.convertHistorySyncMessage( ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, ) + // This is a hack to remove duplicates where the same message is inserted with both the LID and phone number sender + // TODO prevent those being inserted in the first place instead of hacking around it here + if !dups.Add(converted.ID) { + continue + } convertedMessages = append(convertedMessages, converted) if mediaReq != nil { mediaRequests = append(mediaRequests, mediaReq) From a13989e656eee32eebfbbbca88e57865d6e3c68c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 10 Aug 2026 15:08:24 +0300 Subject: [PATCH 021/139] wadb/message: don't include anchor when querying backfill messages --- pkg/connector/wadb/message.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/connector/wadb/message.go b/pkg/connector/wadb/message.go index 4b16002..7a6c63d 100644 --- a/pkg/connector/wadb/message.go +++ b/pkg/connector/wadb/message.go @@ -96,12 +96,12 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo args := []any{mq.BridgeID, loginID, chatJID} argNum := 4 if startTime != nil { - whereClauses += fmt.Sprintf(" AND timestamp >= $%d", argNum) + whereClauses += fmt.Sprintf(" AND timestamp > $%d", argNum) args = append(args, startTime.Unix()) argNum++ } if endTime != nil { - whereClauses += fmt.Sprintf(" AND timestamp <= $%d", argNum) + whereClauses += fmt.Sprintf(" AND timestamp < $%d", argNum) args = append(args, endTime.Unix()) } From 1ee6deeb9e7622cd41d128d918f0b953fea0f940 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 11 Aug 2026 13:06:59 +0300 Subject: [PATCH 022/139] handlewhatsapp: ignore newsletter messages when bridging isn't possible --- pkg/connector/client.go | 4 ++++ pkg/connector/handlewhatsapp.go | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index c5a9348..08c71c5 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -56,6 +56,8 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. 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 @@ -122,6 +124,8 @@ type WhatsAppClient struct { lastPresence types.Presence createDedup *exsync.Set[types.MessageID] + disableNewsletter bool + appStateRecoveryLock sync.Mutex appStateFullSyncAttempted map[appstate.WAPatchName]time.Time } diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index fce47df..ef2012f 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -285,6 +285,9 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast { return } + if evt.Info.Chat.Server == types.NewsletterServer && wa.disableNewsletter { + return + } if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { return false } @@ -782,6 +785,9 @@ 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, From 07f0c835094eb64ee45a5864ef7324cffa5e8169 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 11 Aug 2026 15:22:56 +0300 Subject: [PATCH 023/139] dependencies: update mautrix-go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2b0f16e..5ad5c40 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/sync v0.22.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.29.1-0.20260804113340-56938b8a508d + maunium.net/go/mautrix v0.29.1-0.20260811114840-3e65f58bc022 ) require ( diff --git a/go.sum b/go.sum index d01face..bdec40e 100644 --- a/go.sum +++ b/go.sum @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.29.1-0.20260804113340-56938b8a508d h1:jbC6tXphot/X1UngnZLv7LcDTzaHBi2phwxrpyhtfrs= -maunium.net/go/mautrix v0.29.1-0.20260804113340-56938b8a508d/go.mod h1:PeLuIih5jnbwb2xKNNi0Te7AtINEQE2Uv3p6iG3RV80= +maunium.net/go/mautrix v0.29.1-0.20260811114840-3e65f58bc022 h1:g6mfG2ZZTI1x9wy/vJBB3R6/gLKq6+tR6h4FDnuW82E= +maunium.net/go/mautrix v0.29.1-0.20260811114840-3e65f58bc022/go.mod h1:PeLuIih5jnbwb2xKNNi0Te7AtINEQE2Uv3p6iG3RV80= From 80d69434fb266d4b4a1ba19d2ea53e6afc93c807 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 14 Aug 2026 15:34:27 +0300 Subject: [PATCH 024/139] dependencies: update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/client.go | 2 -- pkg/connector/handlewhatsapp.go | 3 --- pkg/connector/startchat.go | 5 ----- 5 files changed, 3 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 5ad5c40..593f51e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/tidwall/gjson v1.19.0 go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260806224404-e277b766ab33 + go.mau.fi/whatsmeow v0.0.0-20260814123134-0dcf1f50f4b1 golang.org/x/image v0.44.0 golang.org/x/net v0.57.0 golang.org/x/sync v0.22.0 diff --git a/go.sum b/go.sum index bdec40e..99259c0 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391 h1:lsvBEY8MJfYdV61YbwikiQvb go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= -go.mau.fi/whatsmeow v0.0.0-20260806224404-e277b766ab33 h1:NTzJLmLB8ysR35TXPT8Dl4h2AHumaV4sKDX6JeJuVA4= -go.mau.fi/whatsmeow v0.0.0-20260806224404-e277b766ab33/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= +go.mau.fi/whatsmeow v0.0.0-20260814123134-0dcf1f50f4b1 h1:00/21vJJvBBkG7Ia/3EjE/DBiheg6/HrFyy8jWUQZr0= +go.mau.fi/whatsmeow v0.0.0-20260814123134-0dcf1f50f4b1/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 08c71c5..b3ecdba 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -54,7 +54,6 @@ 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, @@ -122,7 +121,6 @@ type WhatsAppClient struct { isNewLogin bool pushNamesSynced *exsync.Event lastPresence types.Presence - createDedup *exsync.Set[types.MessageID] disableNewsletter bool diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index ef2012f..509c589 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -770,9 +770,6 @@ 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, diff --git a/pkg/connector/startchat.go b/pkg/connector/startchat.go index f0d869b..c123311 100644 --- a/pkg/connector/startchat.go +++ b/pkg/connector/startchat.go @@ -246,14 +246,9 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl } func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) { - createKey := wa.Client.GenerateMessageID() - if params.RoomID != "" { - wa.createDedup.Add(createKey) - } req := whatsmeow.ReqCreateGroup{ Name: ptr.Val(params.Name).Name, Participants: make([]types.JID, len(params.Participants)), - CreateKey: createKey, } for i, participant := range params.Participants { jid := waid.ParseUserID(participant) From e7e5e57d9ed9ea12e3e54c2d9d3dcdc998516fb5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 16 Aug 2026 15:48:54 +0300 Subject: [PATCH 025/139] Bump version to v26.08 --- CHANGELOG.md | 4 +++ cmd/mautrix-whatsapp/main.go | 2 +- go.mod | 28 +++++++++---------- go.sum | 52 ++++++++++++++++++------------------ 4 files changed, 45 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e47e7b2..4d17865 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v26.08 + +* Switched direct chats to use LIDs instead of phone numbers. + # v26.07 * Updated Docker image to Alpine 3.24. diff --git a/cmd/mautrix-whatsapp/main.go b/cmd/mautrix-whatsapp/main.go index 47d971c..011e9b0 100644 --- a/cmd/mautrix-whatsapp/main.go +++ b/cmd/mautrix-whatsapp/main.go @@ -18,7 +18,7 @@ var m = mxmain.BridgeMain{ Name: "mautrix-whatsapp", URL: "https://github.com/mautrix/whatsapp", Description: "A Matrix-WhatsApp puppeting bridge.", - Version: "26.07", + Version: "26.08", SemCalVer: true, Connector: &connector.WhatsAppConnector{}, } diff --git a/go.mod b/go.mod index 593f51e..ad3d58a 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module go.mau.fi/mautrix-whatsapp go 1.25.0 -toolchain go1.26.5 +toolchain go1.26.6 tool go.mau.fi/util/cmd/maubuild @@ -10,15 +10,15 @@ 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.9.12-0.20260719092501-f9c03d846391 + go.mau.fi/util v0.10.0 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260814123134-0dcf1f50f4b1 - golang.org/x/image v0.44.0 - golang.org/x/net v0.57.0 + go.mau.fi/whatsmeow v0.0.0-20260816113502-fb386f152837 + 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.11 + google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.29.1-0.20260811114840-3e65f58bc022 + maunium.net/go/mautrix v0.30.0 ) require ( @@ -31,8 +31,8 @@ require ( 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.48 // indirect - github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca // indirect + github.com/mattn/go-sqlite3 v1.14.49 // indirect + github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b // 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 @@ -40,14 +40,14 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect - github.com/yuin/goldmark v1.8.4 // indirect + github.com/yuin/goldmark v1.8.5 // indirect go.mau.fi/libsignal v0.2.2 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect - golang.org/x/crypto v0.54.0 // indirect - golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect - golang.org/x/mod v0.38.0 // indirect + golang.org/x/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.40.0 // indirect + golang.org/x/text v0.41.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 99259c0..62f54b2 100644 --- a/go.sum +++ b/go.sum @@ -34,10 +34,10 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= -github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= -github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca h1:GHSUVE4yOgX4E7kTRzpxCPbCOYkd3Kj8Dgdod30OI1E= -github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -67,37 +67,37 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= -github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= -github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +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.9.12-0.20260719092501-f9c03d846391 h1:lsvBEY8MJfYdV61YbwikiQvb0Al/onbmLW5wfl/0tag= -go.mau.fi/util v0.9.12-0.20260719092501-f9c03d846391/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= +go.mau.fi/util v0.10.0 h1:vH9IXZmfBKa96p47HxrVqEPkrj02zDJg3o4EF172+Lk= +go.mau.fi/util v0.10.0/go.mod h1:uZwpm9sK4wO2Qqy+t6QoVq29szMsRxWXp9/BkQLG4xk= 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-20260814123134-0dcf1f50f4b1 h1:00/21vJJvBBkG7Ia/3EjE/DBiheg6/HrFyy8jWUQZr0= -go.mau.fi/whatsmeow v0.0.0-20260814123134-0dcf1f50f4b1/go.mod h1:Iy/xVSuVU2payR26MB1hv0UZUWRraEn4qKZ7+VRHulg= +go.mau.fi/whatsmeow v0.0.0-20260816113502-fb386f152837 h1:xJ13dqFcK/oPNImltlk7sumI/uiQ9EON/6g/iwpT1Nc= +go.mau.fi/whatsmeow v0.0.0-20260816113502-fb386f152837/go.mod h1:UFP0D7aj+biuFejsllcoNiHTQorh0ODFUyG4Txpqfks= 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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g= -golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= -golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= -golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/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/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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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= 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 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.29.1-0.20260811114840-3e65f58bc022 h1:g6mfG2ZZTI1x9wy/vJBB3R6/gLKq6+tR6h4FDnuW82E= -maunium.net/go/mautrix v0.29.1-0.20260811114840-3e65f58bc022/go.mod h1:PeLuIih5jnbwb2xKNNi0Te7AtINEQE2Uv3p6iG3RV80= +maunium.net/go/mautrix v0.30.0 h1:bad+q7w5tLqiHpr+oUxVI+8m8ePbV3AvoFKg2jQzPyo= +maunium.net/go/mautrix v0.30.0/go.mod h1:bb0gjxbTFOqTaAYKGw5E7j9XROUR2Sl1Etm3IbmYXbo= From 0f1832595fcd61b859bf4326f50234c5511eac27 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Wed, 19 Aug 2026 15:41:15 +0100 Subject: [PATCH 026/139] login: split connect and phone pair timeouts, increase to 30s (#947) --- pkg/connector/login.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 5da419f..a62b294 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -135,7 +135,8 @@ var ( _ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil) ) -const LoginConnectWait = 15 * time.Second +const LoginConnectWait = 30 * time.Second +const LoginPairPhoneWait = 30 * time.Second func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect) @@ -193,19 +194,21 @@ func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogi } func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { - ctx, cancel := context.WithTimeout(ctx, LoginConnectWait) - defer cancel() err := wl.Client.Connect() if err != nil { wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login") return nil, err } - err = wl.WaitForQRs.Wait(ctx) + connectCtx, cancelConnect := context.WithTimeout(ctx, LoginConnectWait) + err = wl.WaitForQRs.Wait(connectCtx) + cancelConnect() if err != nil { wl.Log.Warn().Err(err).Msg("Timed out waiting for connection") return nil, fmt.Errorf("failed to wait for connection: %w", err) } - pairingCode, err := wl.Client.PairPhone(ctx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)") + pairCtx, cancelPair := context.WithTimeout(ctx, LoginPairPhoneWait) + defer cancelPair() + pairingCode, err := wl.Client.PairPhone(pairCtx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)") if err != nil { wl.Log.Err(err).Msg("Failed to request phone code login") if errors.Is(err, whatsmeow.ErrPhoneNumberTooShort) { From 5ef4c47b444da3b219d58bda4b9cb7758bec1acc Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 19 Aug 2026 18:52:38 +0300 Subject: [PATCH 027/139] chatinfo: always allow poll responses in power levels --- pkg/connector/chatinfo.go | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 1dbebcc..1461561 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -284,14 +284,13 @@ 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.StateRoomName: metaChangePL, + event.StateRoomAvatar: metaChangePL, + event.StateTopic: metaChangePL, + event.EventReaction: defaultPL, + event.EventRedaction: defaultPL, + event.EventUnstablePollResponse: defaultPL, event.StateBeeperDisappearingTimer: metaChangePL, - // TODO always allow poll responses }, }, }, @@ -551,12 +550,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, - // TODO always allow poll responses + event.StateRoomName: adminPL, + event.StateRoomAvatar: adminPL, + event.StateTopic: adminPL, + event.EventReaction: defaultPL, + event.EventRedaction: defaultPL, + event.EventUnstablePollResponse: defaultPL, }, }, }, From ba58807b0f42d702af31faa5d1413779dec23ab2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 20 Aug 2026 16:09:02 +0300 Subject: [PATCH 028/139] backfill: clamp message timestamps in history syncs (#946) --- pkg/connector/backfill.go | 43 +++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 7356d11..bb555e6 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -247,12 +247,14 @@ func (wa *WhatsAppClient) handleWAHistorySync( return c.Stringer("chat_jid", jid) }) - var minTime, maxTime, firstItemTime, lastItemTime time.Time - var minTimeIndex, maxTimeIndex int + var firstItemTime, lastItemTime time.Time ignoredTypes := 0 - messages := make([]*wadb.HistorySyncMessageTuple, 0, len(conv.GetMessages())) - for i, rawMsg := range conv.GetMessages() { + rawMessages := conv.GetMessages() + messages := make([]*wadb.HistorySyncMessageTuple, 0, len(rawMessages)) + allowClamp := conv.GetCommentsCount() == 0 + var newerTS uint64 + for i, rawMsg := range rawMessages { // Don't store messages that will just be skipped. msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage()) if err != nil { @@ -270,20 +272,30 @@ func (wa *WhatsAppClient) handleWAHistorySync( firstItemTime = msgEvt.Info.Timestamp } lastItemTime = msgEvt.Info.Timestamp - if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) { - minTime = msgEvt.Info.Timestamp - minTimeIndex = i - } - if maxTime.IsZero() || msgEvt.Info.Timestamp.After(maxTime) { - maxTime = msgEvt.Info.Timestamp - maxTimeIndex = i - } msgType := getMessageType(msgEvt.Message) if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") { ignoredTypes++ continue } + // Comments (replies) in announcement groups are not ordered by timestamp, so don't clamp them. + if rawMsg.GetMessage().GetCommentMetadata().GetCommentParentKey() != nil { + allowClamp = false + } + // WhatsApp has bugs where some random messages will have timestamps decades in the future. + // To ensure they don't mess up our ordering, require timestamps of older messages to be + // before the previous (newer) message. + if currentTS := rawMsg.GetMessage().GetMessageTimestamp(); newerTS > 0 && allowClamp && currentTS > newerTS { + log.Warn(). + Time("current_ts", time.Unix(int64(currentTS), 0)). + Time("prev_ts", time.Unix(int64(newerTS), 0)). + Int("msg_index", i). + Str("msg_id", rawMsg.GetMessage().GetKey().GetID()). + Msg("Clamping message timestamp") + rawMsg.Message.MessageTimestamp = ptr.Ptr(newerTS) + msgEvt.Info.Timestamp = time.Unix(int64(newerTS), 0) + } + newerTS = rawMsg.GetMessage().GetMessageTimestamp() marshaled, err := proto.Marshal(rawMsg) if err != nil { log.Warn().Err(err). @@ -298,13 +310,8 @@ func (wa *WhatsAppClient) handleWAHistorySync( log.Debug(). Int("wrapped_count", len(messages)). Int("ignored_msg_type_count", ignoredTypes). - Time("lowest_time", minTime). - Int("lowest_time_index", minTimeIndex). - Time("highest_time", maxTime). - Int("highest_time_index", maxTimeIndex). Time("first_item_time", firstItemTime). Time("last_item_time", lastItemTime). - Bool("highest_time_mismatch", firstItemTime != maxTime). Dict("metadata", zerolog.Dict(). Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()). Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()). @@ -320,7 +327,7 @@ func (wa *WhatsAppClient) handleWAHistorySync( Msg("Collected messages to save from history sync conversation") if len(messages) > 0 { - err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime)) + err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, firstItemTime)) if err != nil { if stopOnError { return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err) From bbc9a37b30467c6b7e79def8a5d061e5c5ec0dd1 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 20 Aug 2026 18:37:07 +0300 Subject: [PATCH 029/139] dependencies: bump minimum Go version to 1.26 --- .github/workflows/go.yml | 8 ++++---- go.mod | 8 ++++---- go.sum | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 343443b..c9fef73 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,14 +11,14 @@ jobs: strategy: fail-fast: false matrix: - go-version: ["1.25", "1.26"] - name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }} + go-version: ["1.26", "1.27"] + name: Lint ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Go - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go-version }} cache: true diff --git a/go.mod b/go.mod index ad3d58a..1d4c99f 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module go.mau.fi/mautrix-whatsapp -go 1.25.0 +go 1.26.0 -toolchain go1.26.6 +toolchain go1.27.0 tool go.mau.fi/util/cmd/maubuild @@ -10,7 +10,7 @@ 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.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-20260816113502-fb386f152837 golang.org/x/image v0.45.0 @@ -18,7 +18,7 @@ require ( golang.org/x/sync v0.22.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.30.0 + maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e ) require ( diff --git a/go.sum b/go.sum index 62f54b2..9a666fc 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ 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.0 h1:vH9IXZmfBKa96p47HxrVqEPkrj02zDJg3o4EF172+Lk= -go.mau.fi/util v0.10.0/go.mod h1:uZwpm9sK4wO2Qqy+t6QoVq29szMsRxWXp9/BkQLG4xk= +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-20260816113502-fb386f152837 h1:xJ13dqFcK/oPNImltlk7sumI/uiQ9EON/6g/iwpT1Nc= @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.30.0 h1:bad+q7w5tLqiHpr+oUxVI+8m8ePbV3AvoFKg2jQzPyo= -maunium.net/go/mautrix v0.30.0/go.mod h1:bb0gjxbTFOqTaAYKGw5E7j9XROUR2Sl1Etm3IbmYXbo= +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= From d70eb6b135a53a6843f17f42b1d909dc061ff6e3 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 21 Aug 2026 17:25:12 +0300 Subject: [PATCH 030/139] dependencies: update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1d4c99f..f7a6451 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( 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-20260816113502-fb386f152837 + 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 diff --git a/go.sum b/go.sum index 9a666fc..1adbb52 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbs 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-20260816113502-fb386f152837 h1:xJ13dqFcK/oPNImltlk7sumI/uiQ9EON/6g/iwpT1Nc= -go.mau.fi/whatsmeow v0.0.0-20260816113502-fb386f152837/go.mod h1:UFP0D7aj+biuFejsllcoNiHTQorh0ODFUyG4Txpqfks= +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= 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= From 03eb3900826932fa4c661997df03bceb233cf6e2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 24 Aug 2026 13:28:23 +0300 Subject: [PATCH 031/139] backfill: return no more messages flag if there's no anchor message --- pkg/connector/backfill.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index bb555e6..cab460f 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -532,6 +532,7 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet return nil, fmt.Errorf("failed to load messages from database: %w", err) } else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) { wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0) + hasMore = hasMore && params.AnchorMessage != nil if hasMore && !params.AllowSlowFetch { return &bridgev2.FetchMessagesResponse{ MoreRequiresSlowFetch: true, From e1220a8d4eb80dae42e01c8e9fcabe22aeae23fc Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 24 Aug 2026 13:30:35 +0300 Subject: [PATCH 032/139] handlewhatsapp: delete history sync data on logout --- pkg/connector/client.go | 4 ++++ pkg/connector/handlewhatsapp.go | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index b3ecdba..f0a5947 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -393,6 +393,10 @@ func (wa *WhatsAppClient) LogoutRemote(ctx context.Context) { } wa.Disconnect() wa.Client = nil + err := wa.Main.DB.Conversation.DeleteAll(ctx, wa.UserLogin.ID) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to delete history sync data on logout") + } } func (wa *WhatsAppClient) IsLoggedIn() bool { diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 509c589..130c275 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -202,7 +202,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) wa.notifyOfflineSyncWaiter(nil) case *events.LoggedOut: - wa.handleWALogout(evt.Reason, evt.OnConnect) + wa.handleWALogout(ctx, evt.Reason, evt.OnConnect) wa.notifyOfflineSyncWaiter(fmt.Errorf("logged out: %s", evt.Reason)) case *events.Disconnected: // Don't send the normal transient disconnect state if we're already in a different transient disconnect state. @@ -528,7 +528,7 @@ func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events. }) } -func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onConnect bool) { +func (wa *WhatsAppClient) handleWALogout(ctx context.Context, reason events.ConnectFailureReason, onConnect bool) { errorCode := WAUnknownLogout if reason == events.ConnectFailureLoggedOut { errorCode = WALoggedOut @@ -540,6 +540,10 @@ func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onC wa.JID = types.EmptyJID wa.LID = types.EmptyJID wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0 + err := wa.Main.DB.Conversation.DeleteAll(ctx, wa.UserLogin.ID) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to delete history sync data on logout") + } wa.UserLogin.BridgeState.Send(status.BridgeState{ StateEvent: status.StateBadCredentials, Error: errorCode, From 39bc900ac930a060041bb819ecda0ffd8e502189 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Mon, 24 Aug 2026 11:33:43 +0100 Subject: [PATCH 033/139] dependencies: update mautrix-go --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index f7a6451..db7c17f 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/sync v0.22.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e + maunium.net/go/mautrix v0.30.1-0.20260822101838-bdb58f2e6c0a ) require ( diff --git a/go.sum b/go.sum index 1adbb52..6eca722 100644 --- a/go.sum +++ b/go.sum @@ -109,3 +109,5 @@ 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.30.1-0.20260822101838-bdb58f2e6c0a h1:BYeNDQQLR0edhy6Smq41T3QBNWn6FhrNtRG8CZTuUV4= +maunium.net/go/mautrix v0.30.1-0.20260822101838-bdb58f2e6c0a/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= From 6f767fb951fd0190c754d045c5d879a1f574a7a2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 24 Aug 2026 16:46:43 +0300 Subject: [PATCH 034/139] dependencies: fix go mod tidy --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 6eca722..f97f0a2 100644 --- a/go.sum +++ b/go.sum @@ -107,7 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.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.30.1-0.20260822101838-bdb58f2e6c0a h1:BYeNDQQLR0edhy6Smq41T3QBNWn6FhrNtRG8CZTuUV4= maunium.net/go/mautrix v0.30.1-0.20260822101838-bdb58f2e6c0a/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= From c4aabf0b1a819d7e8a5555ea62e12a0914b4253f Mon Sep 17 00:00:00 2001 From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com> Date: Mon, 24 Aug 2026 06:51:06 -0700 Subject: [PATCH 035/139] handlewhatsapp: fix nested HD media caption edits (#948) --- pkg/connector/handlewhatsapp.go | 35 ++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 130c275..5508334 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -27,6 +27,7 @@ import ( "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/appstate" + "go.mau.fi/whatsmeow/proto/waCommon" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" @@ -341,18 +342,8 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation() if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD { parentKey := messageAssoc.GetParentMessageKey() - protocolMsg := 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 - } + protocolMsg, shouldHideEdit := makeHDMediaReplacementEdit(evt.Message, parentKey) + dontRenderEdited = shouldHideEdit wa.UserLogin.Log.Debug(). Str("message_id", evt.Info.ID). Str("parent_id", parentKey.GetID()). @@ -386,6 +377,26 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa return res.Success } +func makeHDMediaReplacementEdit(message *waE2E.Message, parentKey *waCommon.MessageKey) (*waE2E.ProtocolMessage, bool) { + protocolMsg := message.GetProtocolMessage() + associatedMessage := message.GetAssociatedChildMessage().GetMessage() + if protocolMsg.GetType() != waE2E.ProtocolMessage_MESSAGE_EDIT || protocolMsg.GetKey() == nil { + protocolMsg = associatedMessage.GetProtocolMessage() + } + if protocolMsg.GetType() == waE2E.ProtocolMessage_MESSAGE_EDIT && protocolMsg.GetKey() != nil { + if child := protocolMsg.GetEditedMessage().GetAssociatedChildMessage().GetMessage(); child != nil { + protocolMsg.EditedMessage = child + } + protocolMsg.Key = parentKey + return protocolMsg, false + } + return &waE2E.ProtocolMessage{ + Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(), + Key: parentKey, + EditedMessage: associatedMessage, + }, true +} + func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool { if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { return false From 508b35e1ae2533605cb3ba39bfd9f131ce652bb0 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Tue, 25 Aug 2026 12:33:30 +0100 Subject: [PATCH 036/139] directmedia: mark media retry decrypt failures (#939) Co-authored-by: Nick Mills-Barrett --- pkg/connector/directmedia.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index 8a7afd9..a875fab 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -255,10 +255,11 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse( type directMediaRetry struct { sync.Mutex - resultURL string - wait *exsync.Event - requested bool - resultType waMmsRetry.MediaRetryNotification_ResultType + resultURL string + wait *exsync.Event + requested bool + resultType waMmsRetry.MediaRetryNotification_ResultType + decryptFail bool } func (wa *WhatsAppClient) getDirectMediaRetryState(msgID networkid.MessageID, create bool) *directMediaRetry { @@ -285,6 +286,9 @@ func (wa *WhatsAppClient) requestAndWaitDirectMedia(ctx context.Context, rawMsgI keys.DirectPath = state.resultURL return nil } + if state.decryptFail { + return mautrix.MNotFound.WithMessage("Unable to retrieve media: failed to decrypt the media retry notification from your phone.") + } switch state.resultType { case waMmsRetry.MediaRetryNotification_NOT_FOUND: return mautrix.MNotFound.WithMessage("This media was not found on your phone.") @@ -338,6 +342,9 @@ func (wa *WhatsAppClient) receiveDirectMediaRetry(ctx context.Context, msg *data retryData, err := whatsmeow.DecryptMediaRetryNotification(retry, keys.Key) if err != nil { log.Warn().Err(err).Msg("Failed to decrypt media retry notification") + if state != nil { + state.decryptFail = true + } return } if state != nil { From 4973412350b2b3231295c4b685a1e6ec8f45e961 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 25 Aug 2026 17:30:08 +0300 Subject: [PATCH 037/139] dependencies: update mautrix-go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index db7c17f..6be8397 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/sync v0.22.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.30.1-0.20260822101838-bdb58f2e6c0a + maunium.net/go/mautrix v0.30.1-0.20260825141151-92fa7b2e4545 ) require ( diff --git a/go.sum b/go.sum index f97f0a2..5b26c4f 100644 --- a/go.sum +++ b/go.sum @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.30.1-0.20260822101838-bdb58f2e6c0a h1:BYeNDQQLR0edhy6Smq41T3QBNWn6FhrNtRG8CZTuUV4= -maunium.net/go/mautrix v0.30.1-0.20260822101838-bdb58f2e6c0a/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= +maunium.net/go/mautrix v0.30.1-0.20260825141151-92fa7b2e4545 h1:cVpn8+sWu96w/lGLl6pO9t7hcuEouXbIQiOtnH58JwI= +maunium.net/go/mautrix v0.30.1-0.20260825141151-92fa7b2e4545/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= From e59633c99192bcac117e830d83363b10826a1eb6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 25 Aug 2026 23:56:48 +0300 Subject: [PATCH 038/139] handlewhatsapp: fix handling newsletter read receipts --- pkg/connector/handlewhatsapp.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 5508334..4ca2217 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -24,6 +24,7 @@ import ( "time" "github.com/rs/zerolog" + "go.mau.fi/util/exslices" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/appstate" @@ -476,6 +477,8 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei messageSender := wa.GetLID() if !evt.MessageSender.IsEmpty() { messageSender = evt.MessageSender + } else if evt.Chat.Server == types.NewsletterServer { + messageSender = evt.Chat } var chatAlt types.JID if evt.Chat.Server == types.DefaultUserServer { @@ -494,6 +497,8 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei senderLID := evt.Sender if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() { senderLID = evt.SenderAlt + } else if evt.Chat.Server == types.NewsletterServer && evt.Type == types.ReceiptTypeReadSelf { + senderLID = wa.GetLID() } res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{ EventMeta: simplevent.EventMeta{ @@ -501,6 +506,11 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei PortalKey: wa.makeWAPortalKey(evt.Chat), Sender: wa.makeEventSender(ctx, senderLID), Timestamp: evt.Timestamp, + LogContext: func(c zerolog.Context) zerolog.Context { + return c. + Strs("targets", exslices.CastToString[string](targets)). + Stringer("receipt_sender", senderLID) + }, }, Targets: targets, }) From a7bf2c89f8691d8a4ab1851cdc7b49d063c4bdd4 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 29 Aug 2026 01:44:57 +0300 Subject: [PATCH 039/139] dependencies: update --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 6be8397..9a26127 100644 --- a/go.mod +++ b/go.mod @@ -12,13 +12,13 @@ require ( 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 + go.mau.fi/whatsmeow v0.0.0-20260828224850-0fadda796019 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 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.30.1-0.20260825141151-92fa7b2e4545 + maunium.net/go/mautrix v0.30.1-0.20260828224424-c22bd4fdc9e6 ) require ( @@ -31,8 +31,8 @@ require ( 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.50 // indirect + github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 // 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 @@ -44,7 +44,7 @@ require ( go.mau.fi/libsignal v0.2.2 // 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/exp v0.0.0-20260824195058-e88cd73687aa // 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 diff --git a/go.sum b/go.sum index 5b26c4f..e5827da 100644 --- a/go.sum +++ b/go.sum @@ -34,10 +34,10 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.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.50 h1:dmdFvo1XG4MPzA4IkAmE9upVz/Nj31uRoM5+jC8hYbY= +github.com/mattn/go-sqlite3 v1.14.50/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= +github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 h1:lcWAnrqr2nNfDiArwFNHCE4787Mw2tCdVSOXCru0/0E= +github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -75,14 +75,14 @@ go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbs 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= +go.mau.fi/whatsmeow v0.0.0-20260828224850-0fadda796019 h1:vUX4rgZhobwtMYH6tkYOSyfd2ufTysNEDWVpTSVPjmc= +go.mau.fi/whatsmeow v0.0.0-20260828224850-0fadda796019/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= 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/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= 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= @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.30.1-0.20260825141151-92fa7b2e4545 h1:cVpn8+sWu96w/lGLl6pO9t7hcuEouXbIQiOtnH58JwI= -maunium.net/go/mautrix v0.30.1-0.20260825141151-92fa7b2e4545/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= +maunium.net/go/mautrix v0.30.1-0.20260828224424-c22bd4fdc9e6 h1:mL1BquGn+7efD4ECX62uHVN2MnsrC65JuOgxDdKFF7o= +maunium.net/go/mautrix v0.30.1-0.20260828224424-c22bd4fdc9e6/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= From d1dbe393b6875a1997a8b4f6f1dd54b33ad7fec9 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 3 Sep 2026 14:17:20 +0300 Subject: [PATCH 040/139] dependencies: update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9a26127..288eb6f 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( 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-20260828224850-0fadda796019 + go.mau.fi/whatsmeow v0.0.0-20260903111606-de26b4ab6499 golang.org/x/image v0.45.0 golang.org/x/net v0.58.0 golang.org/x/sync v0.22.0 diff --git a/go.sum b/go.sum index e5827da..e5d6221 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbs 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-20260828224850-0fadda796019 h1:vUX4rgZhobwtMYH6tkYOSyfd2ufTysNEDWVpTSVPjmc= -go.mau.fi/whatsmeow v0.0.0-20260828224850-0fadda796019/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= +go.mau.fi/whatsmeow v0.0.0-20260903111606-de26b4ab6499 h1:7RG3BeC9qXh2aQ0RwjIFZxhCwTohdtNzJaKHxna32pE= +go.mau.fi/whatsmeow v0.0.0-20260903111606-de26b4ab6499/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= 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= From 51504db3098ec1c7b81c314f01a7d0458ac309e0 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 3 Sep 2026 02:17:28 +0300 Subject: [PATCH 041/139] chatinfo: add option to limit eager member sync in groups --- pkg/connector/chatinfo.go | 6 +++++- pkg/connector/config.go | 2 ++ pkg/connector/example-config.yaml | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 1461561..675a5ce 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -271,11 +271,12 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn setAddressingMode(info.AddressingMode), setTopicID(info.TopicID, info.Topic), ) + syncAllMembers := wa.Main.Config.MaxMemberSync < 0 || len(info.Participants) < wa.Main.Config.MaxMemberSync wrapped := &bridgev2.ChatInfo{ Name: ptr.Ptr(info.Name), Topic: ptr.Ptr(info.Topic), Members: &bridgev2.ChatMemberList{ - IsFull: !info.IsIncognito && !info.IsParent, + IsFull: !info.IsIncognito && !info.IsParent && syncAllMembers, TotalMemberCount: len(info.Participants), MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)), PowerLevels: &bridgev2.PowerLevelOverrides{ @@ -315,6 +316,9 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn } else if pcp.IsAdmin { member.PowerLevel = ptr.Ptr(adminPL) } else { + if !syncAllMembers && !member.EventSender.IsFromMe { + continue + } member.PowerLevel = ptr.Ptr(defaultPL) } member.MemberEventExtra = map[string]any{ diff --git a/pkg/connector/config.go b/pkg/connector/config.go index e51c80a..d069598 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -52,6 +52,7 @@ type Config struct { DirectMediaAutoRequest bool `yaml:"direct_media_auto_request"` InitialAutoReconnect bool `yaml:"initial_auto_reconnect"` UseWhatsAppRetryStore bool `yaml:"use_whatsapp_retry_store"` + MaxMemberSync int `yaml:"max_member_sync"` AnimatedSticker msgconv.AnimatedStickerConfig `yaml:"animated_sticker"` @@ -129,6 +130,7 @@ func upgradeConfig(helper up.Helper) { helper.Copy(up.Bool, "direct_media_auto_request") helper.Copy(up.Bool, "initial_auto_reconnect") helper.Copy(up.Bool, "use_whatsapp_retry_store") + helper.Copy(up.Int, "max_member_sync") helper.Copy(up.Str, "animated_sticker", "target") helper.Copy(up.Int, "animated_sticker", "args", "width") diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index 564f25e..d277383 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -69,6 +69,9 @@ initial_auto_reconnect: true # the bridge? By default, the bridge only stores messages in memory, and therefore can't accept # retry receipts if the bridge is restarted after the message is sent. use_whatsapp_retry_store: false +# Maximum size of groups to sync members eagerly in. Defaults to unlimited. +# If set, groups with more than this number of members will not have non-admin participants synced until they talk. +max_member_sync: -1 # Settings for converting animated stickers. animated_sticker: From 6302a1c709e48aaca3cd6b272f3ca4a6ea2cd080 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 3 Sep 2026 14:29:17 +0300 Subject: [PATCH 042/139] userinfo: add option to lazily fetch avatars using direct media --- pkg/connector/config.go | 3 ++ pkg/connector/connector.go | 3 ++ pkg/connector/directmedia.go | 21 ++++++++++++-- pkg/connector/example-config.yaml | 3 ++ pkg/connector/handlewhatsapp.go | 2 +- pkg/connector/startchat.go | 8 +++--- pkg/connector/userinfo.go | 46 ++++++++++++++++++++++++++----- pkg/connector/wadb/avatarcache.go | 14 ++++++++++ pkg/waid/dbmeta.go | 22 ++++++++------- pkg/waid/mediaid.go | 11 ++++++++ 10 files changed, 108 insertions(+), 25 deletions(-) diff --git a/pkg/connector/config.go b/pkg/connector/config.go index d069598..24874a2 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -53,6 +53,7 @@ type Config struct { InitialAutoReconnect bool `yaml:"initial_auto_reconnect"` UseWhatsAppRetryStore bool `yaml:"use_whatsapp_retry_store"` MaxMemberSync int `yaml:"max_member_sync"` + LazyAvatars bool `yaml:"lazy_avatars"` AnimatedSticker msgconv.AnimatedStickerConfig `yaml:"animated_sticker"` @@ -131,6 +132,7 @@ func upgradeConfig(helper up.Helper) { helper.Copy(up.Bool, "initial_auto_reconnect") helper.Copy(up.Bool, "use_whatsapp_retry_store") helper.Copy(up.Int, "max_member_sync") + helper.Copy(up.Bool, "lazy_avatars") helper.Copy(up.Str, "animated_sticker", "target") helper.Copy(up.Int, "animated_sticker", "args", "width") @@ -207,6 +209,7 @@ func (wa *WhatsAppConnector) GetConfig() (string, any, up.Upgrader) { {"proxy"}, {"displayname_template"}, {"call_start_notices"}, + {"animated_sticker"}, {"history_sync"}, }, Base: ExampleConfig, diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index e262c82..193dc4d 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -142,6 +142,9 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) { } func (wa *WhatsAppConnector) Start(ctx context.Context) error { + if !wa.MsgConv.DirectMedia && wa.Config.LazyAvatars { + return fmt.Errorf("lazy_avatars set without enabling global direct_media") + } err := wa.DeviceStore.Upgrade(ctx) if err != nil { return bridgev2.DBUpgradeError{Err: err, Section: "whatsmeow"} diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index a875fab..092ce3e 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -30,6 +30,7 @@ import ( "github.com/rs/zerolog" "go.mau.fi/util/exsync" + "go.mau.fi/util/jsontime" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waMmsRetry" @@ -88,9 +89,9 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars if err != nil { return nil, fmt.Errorf("failed to get avatar cache entry: %w", err) } - if cachedInfo != nil && cachedInfo.Gone { + if cachedInfo.IsGone() { return nil, mautrix.MNotFound.WithMessage("Avatar is no longer available (cached response)") - } else if cachedInfo == nil || cachedInfo.Expiry.Time.Before(time.Now().Add(5*time.Minute)) { + } else if cachedInfo.Expired() { zerolog.Ctx(ctx).Debug(). Str("avatar_id", parsedID.Avatar.AvatarID). Msg("Refreshing avatar URL from WhatsApp servers") @@ -99,7 +100,7 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars }) if errors.Is(err, whatsmeow.ErrProfilePictureNotSet) || errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) || - (err == nil && (avatar == nil || avatar.ID != parsedID.Avatar.AvatarID)) { + (err == nil && (avatar == nil || (avatar.ID != parsedID.Avatar.AvatarID && !parsedID.Avatar.IsRandom()))) { zerolog.Ctx(ctx).Debug(). Err(err). Stringer("target_jid", parsedID.Avatar.TargetJID). @@ -107,9 +108,14 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars Str("wanted_avatar_id", parsedID.Avatar.AvatarID). Str("got_avatar_id", ptr.Val(avatar).ID). Msg("Avatar is no longer available") + var goneExpiry jsontime.Unix + if parsedID.Avatar.IsRandom() { + goneExpiry = jsontime.U(time.Now().Add(7 * 24 * time.Hour)) + } err = wa.DB.AvatarCache.Put(ctx, &wadb.AvatarCacheEntry{ EntityJID: parsedID.Avatar.TargetJID, AvatarID: parsedID.Avatar.AvatarID, + Expiry: goneExpiry, Gone: true, }) if err != nil { @@ -128,6 +134,15 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars Str("avatar_id", avatar.ID). Msg("Failed to update avatar cache entry") } + if cachedInfo.AvatarID != parsedID.Avatar.AvatarID { + cachedInfo.AvatarID = parsedID.Avatar.AvatarID + err = wa.DB.AvatarCache.Put(ctx, cachedInfo) + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err). + Str("avatar_id", parsedID.Avatar.AvatarID). + Msg("Failed to update avatar cache entry") + } + } } return &mediaproxy.GetMediaResponseFile{ Callback: func(w *os.File) (*mediaproxy.FileMeta, error) { diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index d277383..905e6d6 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -72,6 +72,9 @@ use_whatsapp_retry_store: false # Maximum size of groups to sync members eagerly in. Defaults to unlimited. # If set, groups with more than this number of members will not have non-admin participants synced until they talk. max_member_sync: -1 +# Lazily load avatars using direct media? +# Global direct media must be enabled for this. +lazy_avatars: false # Settings for converting animated stickers. animated_sticker: diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 4ca2217..8a758eb 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -729,7 +729,7 @@ func (wa *WhatsAppClient) syncGhost(jid types.JID, reason string, pictureID *str if pictureID != nil && *pictureID != "" && ghost.AvatarID == networkid.AvatarID(*pictureID) { return } - userInfo, err := wa.getUserInfo(ctx, jid, pictureID != nil) + userInfo, err := wa.getUserInfo(ctx, jid, ptr.Val(pictureID), pictureID != nil) if err != nil { log.Err(err).Msg("Failed to get user info") } else { diff --git a/pkg/connector/startchat.go b/pkg/connector/startchat.go index c123311..2ba1e09 100644 --- a/pkg/connector/startchat.go +++ b/pkg/connector/startchat.go @@ -64,12 +64,12 @@ func looksEmaily(str string) bool { return false } -type cacheEntry struct { +type isOnWhatsappCacheEntry struct { jid types.JID ts time.Time } -var isOnWhatsappCache = exsync.NewMap[string, cacheEntry]() +var isOnWhatsappCache = exsync.NewMap[string, isOnWhatsappCacheEntry]() func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) (types.JID, error) { if strings.HasSuffix(number, "@"+types.BotServer) || strings.HasSuffix(number, "@"+types.HiddenUserServer) { @@ -94,7 +94,7 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) } else if !resp[0].IsIn { return types.EmptyJID, bridgev2.WrapRespErr(fmt.Errorf("the server said +%s is not on WhatsApp", resp[0].JID.User), mautrix.MNotFound) } else { - isOnWhatsappCache.Set(number, cacheEntry{resp[0].JID, time.Now()}) + isOnWhatsappCache.Set(number, isOnWhatsappCacheEntry{resp[0].JID, time.Now()}) return resp[0].JID, nil } } @@ -238,7 +238,7 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl resp = append(resp, &bridgev2.ResolveIdentifierResponse{ Ghost: ghost, UserID: waid.MakeUserID(jid), - UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, false), + UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, "", false), Chat: chatResp, }) } diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index e827955..05d6c40 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -165,7 +165,7 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID log.Warn().Stringer("jid", jid).Msg("Didn't get info for puppet in background sync") continue } - userInfo, err := wa.getUserInfo(ctx, jid, info.PictureID != "" && string(ghost.AvatarID) != info.PictureID) + userInfo, err := wa.getUserInfo(ctx, jid, info.PictureID, info.PictureID != "" && string(ghost.AvatarID) != info.PictureID) if err != nil { log.Err(err).Stringer("jid", jid).Msg("Failed to get user info for puppet in background sync") continue @@ -181,18 +181,18 @@ func (wa *WhatsAppClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost return nil, nil } jid := waid.ParseUserID(ghost.ID) - return wa.getUserInfo(ctx, jid, ghost.AvatarID == "") + return wa.getUserInfo(ctx, jid, "", ghost.AvatarID == "") } -func (wa *WhatsAppClient) getUserInfo(ctx context.Context, jid types.JID, fetchAvatar bool) (*bridgev2.UserInfo, error) { +func (wa *WhatsAppClient) getUserInfo(ctx context.Context, jid types.JID, avatarID string, fetchAvatar bool) (*bridgev2.UserInfo, error) { contact, err := wa.GetStore().Contacts.GetContact(ctx, jid) if err != nil { return nil, err } - return wa.contactToUserInfo(ctx, jid, contact, fetchAvatar), nil + return wa.contactToUserInfo(ctx, jid, contact, avatarID, fetchAvatar), nil } -func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, contact types.ContactInfo, getAvatar bool) *bridgev2.UserInfo { +func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, contact types.ContactInfo, avatarID string, fetchAvatar bool) *bridgev2.UserInfo { if jid == types.MetaAIJID && contact.PushName == jid.User { contact.PushName = "Meta AI" } else if jid == types.LegacyPSAJID || jid == types.PSAJID { @@ -270,7 +270,9 @@ func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, } else if phone != "" { ui.Identifiers = []string{fmt.Sprintf("tel:%s", phone)} } - if getAvatar { + if wa.Main.Config.LazyAvatars { + ui.ExtraUpdates = bridgev2.MergeExtraUpdaters(ui.ExtraUpdates, wa.makeLazyGhostAvatarUpdater(avatarID, fetchAvatar)) + } else if fetchAvatar { ui.ExtraUpdates = bridgev2.MergeExtraUpdaters(ui.ExtraUpdates, wa.fetchGhostAvatar) } return ui @@ -307,6 +309,21 @@ func avatarInfoToCacheEntry(ctx context.Context, jid types.JID, avatar *types.Pr } } +func (wa *WhatsAppClient) makeLazyDirectMediaAvatar(ctx context.Context, jid types.JID, avatarID string, community bool) (*bridgev2.Avatar, error) { + if avatarID == "" { + avatarID = waid.MakeRandomAvatarID() + } + mxc, err := wa.Main.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeAvatarMediaID(jid, avatarID, wa.UserLogin.ID, community)) + if err != nil { + return nil, fmt.Errorf("failed to generate MXC URI: %w", err) + } + return &bridgev2.Avatar{ + ID: networkid.AvatarID(avatarID), + MXC: mxc, + Hash: sha256.Sum256([]byte(avatarID)), + }, nil +} + func (wa *WhatsAppClient) makeDirectMediaAvatar(ctx context.Context, jid types.JID, avatar *types.ProfilePictureInfo, community bool) (*bridgev2.Avatar, error) { mxc, err := wa.Main.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeAvatarMediaID(jid, avatar.ID, wa.UserLogin.ID, community)) if err != nil { @@ -328,6 +345,21 @@ func (wa *WhatsAppClient) makeDirectMediaAvatar(ctx context.Context, jid types.J }, nil } +func (wa *WhatsAppClient) makeLazyGhostAvatarUpdater(avatarID string, forceUpdate bool) func(context.Context, *bridgev2.Ghost) bool { + return func(ctx context.Context, ghost *bridgev2.Ghost) bool { + if ghost.AvatarID != "" && (networkid.AvatarID(avatarID) == ghost.AvatarID || (!forceUpdate && avatarID == "")) { + return false + } + jid := waid.ParseUserID(ghost.ID) + wrappedAvatar, err := wa.makeLazyDirectMediaAvatar(ctx, jid, avatarID, false) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to prepare lazy direct media avatar") + return false + } + return ghost.UpdateAvatar(ctx, wrappedAvatar) + } +} + func (wa *WhatsAppClient) fetchGhostAvatar(ctx context.Context, ghost *bridgev2.Ghost) bool { jid := waid.ParseUserID(ghost.ID) existingID := string(ghost.AvatarID) @@ -399,7 +431,7 @@ func (wa *WhatsAppClient) resyncContacts(forceAvatarSync, automatic bool) { } else if contact, err := contactStore.GetContact(ctx, jid); err != nil { log.Err(err).Stringer("jid", jid).Msg("Failed to get contact info") } else { - userInfo := wa.contactToUserInfo(ctx, jid, contact, forceAvatarSync || ghost.AvatarID == "") + userInfo := wa.contactToUserInfo(ctx, jid, contact, "", forceAvatarSync || ghost.AvatarID == "") ghost.UpdateInfo(ctx, userInfo) wa.syncAltGhostWithInfo(ctx, jid, userInfo) } diff --git a/pkg/connector/wadb/avatarcache.go b/pkg/connector/wadb/avatarcache.go index 8ea947f..55fb4b9 100644 --- a/pkg/connector/wadb/avatarcache.go +++ b/pkg/connector/wadb/avatarcache.go @@ -2,10 +2,14 @@ package wadb import ( "context" + "strings" + "time" "go.mau.fi/util/dbutil" "go.mau.fi/util/jsontime" "go.mau.fi/whatsmeow/types" + + "go.mau.fi/mautrix-whatsapp/pkg/waid" ) type AvatarCacheQuery struct { @@ -49,3 +53,13 @@ func (ace *AvatarCacheEntry) Scan(row dbutil.Scannable) (*AvatarCacheEntry, erro func (ace *AvatarCacheEntry) sqlVariables() []any { return []any{ace.EntityJID, ace.AvatarID, ace.DirectPath, ace.Expiry, ace.Gone} } + +func (ace *AvatarCacheEntry) IsGone() bool { + return ace != nil && ace.Gone && + // Random IDs can be retried after a specific expiry time, other types of gones can't + (ace.Expiry.IsZero() || !strings.HasPrefix(ace.AvatarID, waid.RandomAvatarIDPrefix) || ace.Expiry.After(time.Now())) +} + +func (ace *AvatarCacheEntry) Expired() bool { + return ace == nil || ace.Expiry.Before(time.Now().Add(5*time.Minute)) +} diff --git a/pkg/waid/dbmeta.go b/pkg/waid/dbmeta.go index 1e54426..e2b60f7 100644 --- a/pkg/waid/dbmeta.go +++ b/pkg/waid/dbmeta.go @@ -37,12 +37,12 @@ type UserLoginMetadata struct { PushKeys *PushKeys `json:"push_keys,omitempty"` APNSEncPubKey []byte `json:"apns_enc_pubkey,omitempty"` APNSEncPrivKey []byte `json:"apns_enc_privkey,omitempty"` - LoggedInAt jsontime.Unix `json:"logged_in_at,omitempty"` + LoggedInAt jsontime.Unix `json:"logged_in_at,omitzero"` AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"` - HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"` - ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitempty"` + HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitzero"` + ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitzero"` MData json.RawMessage `json:"mdata,omitempty"` } @@ -88,13 +88,13 @@ type GroupInviteMeta struct { } type MessageMetadata struct { - SenderDeviceID uint16 `json:"sender_device_id,omitempty"` + SenderDeviceID uint16 `json:"sender_device_id,omitzero"` Error MessageErrorType `json:"error,omitempty"` BroadcastListJID *types.JID `json:"broadcast_list_jid,omitempty"` GroupInvite *GroupInviteMeta `json:"group_invite,omitempty"` FailedMediaMeta json.RawMessage `json:"media_meta,omitempty"` DirectMediaMeta json.RawMessage `json:"direct_media_meta,omitempty"` - IsMatrixPoll bool `json:"is_matrix_poll,omitempty"` + IsMatrixPoll bool `json:"is_matrix_poll,omitzero"` Edits []types.MessageID `json:"edits,omitempty"` } @@ -122,14 +122,16 @@ type ReactionMetadata struct { } type PortalMetadata struct { - DisappearingTimerSetAt int64 `json:"disappearing_timer_set_at,omitempty"` + DisappearingTimerSetAt int64 `json:"disappearing_timer_set_at,omitzero"` TopicID string `json:"topic_id,omitempty"` - LastSync jsontime.Unix `json:"last_sync,omitempty"` - CommunityAnnouncementGroup bool `json:"is_cag,omitempty"` + LastSync jsontime.Unix `json:"last_sync,omitzero"` + CommunityAnnouncementGroup bool `json:"is_cag,omitzero"` AddressingMode types.AddressingMode `json:"addressing_mode,omitempty"` - LIDMigrationAttempted bool `json:"lid_migration_attempted,omitempty"` + LIDMigrationAttempted bool `json:"lid_migration_attempted,omitzero"` } type GhostMetadata struct { - LastSync jsontime.Unix `json:"last_sync,omitempty"` + LastSync jsontime.Unix `json:"last_sync,omitzero"` + + DirectAvatarURL string `json:"direct_avatar_url,omitempty"` } diff --git a/pkg/waid/mediaid.go b/pkg/waid/mediaid.go index 716e187..a0ac2d9 100644 --- a/pkg/waid/mediaid.go +++ b/pkg/waid/mediaid.go @@ -24,6 +24,7 @@ import ( "strconv" "strings" + "go.mau.fi/util/random" "go.mau.fi/whatsmeow/types" "maunium.net/go/mautrix/bridgev2/networkid" ) @@ -85,6 +86,16 @@ type AvatarMediaInfo struct { Community bool } +const RandomAvatarIDPrefix = "mxwa-lazy-avatar-" + +func MakeRandomAvatarID() string { + return RandomAvatarIDPrefix + random.String(8) +} + +func (ami *AvatarMediaInfo) IsRandom() bool { + return ami != nil && strings.HasPrefix(ami.AvatarID, RandomAvatarIDPrefix) +} + func MakeStickerPackMediaID(packID string, fileHash []byte, receiver networkid.UserLoginID) networkid.MediaID { receiverID := compactJID(ParseUserLoginID(receiver, 0)) mediaID := make([]byte, 0, 4+len(packID)+len(fileHash)+len(receiverID)) From 240648081e1ac70b6d5a530f57a6d26f317c9480 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 3 Sep 2026 14:37:14 +0300 Subject: [PATCH 043/139] startchat: return user info with resolve identifier requests --- pkg/connector/startchat.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/connector/startchat.go b/pkg/connector/startchat.go index 2ba1e09..18e8326 100644 --- a/pkg/connector/startchat.go +++ b/pkg/connector/startchat.go @@ -176,11 +176,16 @@ func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier stri if err != nil { return nil, fmt.Errorf("failed to get ghost: %w", err) } + userInfo, err := wa.getUserInfo(ctx, jid, "", false) + if err != nil { + return nil, fmt.Errorf("failed to get user info: %w", err) + } return &bridgev2.ResolveIdentifierResponse{ - Ghost: ghost, - UserID: waid.MakeUserID(jid), - Chat: wa.makeCreateChatResponse(ctx, jid, origJID), + Ghost: ghost, + UserID: waid.MakeUserID(jid), + UserInfo: userInfo, + Chat: wa.makeCreateChatResponse(ctx, jid, origJID), }, nil } From 63dd2582b5c11397aecfa938240e094d7fe45d0d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 8 Sep 2026 14:41:40 +0300 Subject: [PATCH 044/139] dependencies: update --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 288eb6f..5b53985 100644 --- a/go.mod +++ b/go.mod @@ -12,13 +12,13 @@ require ( 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-20260903111606-de26b4ab6499 + go.mau.fi/whatsmeow v0.0.0-20260908082135-57796d3d6b41 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 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.30.1-0.20260828224424-c22bd4fdc9e6 + maunium.net/go/mautrix v0.30.1-0.20260908102503-2f2c98079d9e ) require ( diff --git a/go.sum b/go.sum index e5d6221..38bb208 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbs 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-20260903111606-de26b4ab6499 h1:7RG3BeC9qXh2aQ0RwjIFZxhCwTohdtNzJaKHxna32pE= -go.mau.fi/whatsmeow v0.0.0-20260903111606-de26b4ab6499/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= +go.mau.fi/whatsmeow v0.0.0-20260908082135-57796d3d6b41 h1:EUpS1/OiThY4/S1jgG5jGpC7bILOtM/Q1r1b7BI9NrM= +go.mau.fi/whatsmeow v0.0.0-20260908082135-57796d3d6b41/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= 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= @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.30.1-0.20260828224424-c22bd4fdc9e6 h1:mL1BquGn+7efD4ECX62uHVN2MnsrC65JuOgxDdKFF7o= -maunium.net/go/mautrix v0.30.1-0.20260828224424-c22bd4fdc9e6/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= +maunium.net/go/mautrix v0.30.1-0.20260908102503-2f2c98079d9e h1:T4g/2u7ZEbAGkYGNizjYZSltWR5m0DrX5UDWqfZJdS4= +maunium.net/go/mautrix v0.30.1-0.20260908102503-2f2c98079d9e/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= From 446bd22695570a0b13486f41851251772e8697b8 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 8 Sep 2026 14:42:26 +0300 Subject: [PATCH 045/139] lidmigrate: resync missing avatars and reuse info for alt ghost sync --- pkg/connector/connector.go | 10 +++++- pkg/connector/handlewhatsapp.go | 2 +- pkg/connector/lidmigrate.go | 57 +++++++++++++++++++++++++++++++++ pkg/connector/userinfo.go | 23 ++++++++++--- 4 files changed, 86 insertions(+), 6 deletions(-) diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 193dc4d..3602e93 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -160,7 +160,15 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error { return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"} } - return wa.migrateToLIDDMs(ctx) + err = wa.migrateToLIDDMs(ctx) + if err != nil { + return fmt.Errorf("failed to migrate to LID DMs: %w", err) + } + err = wa.syncMismatchingGhosts(ctx) + if err != nil { + return fmt.Errorf("failed to sync mismatching ghosts: %w", err) + } + return nil } func (wa *WhatsAppConnector) Stop() { diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 8a758eb..d8df69f 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -735,7 +735,7 @@ func (wa *WhatsAppClient) syncGhost(jid types.JID, reason string, pictureID *str } else { ghost.UpdateInfo(ctx, userInfo) log.Debug().Msg("Synced ghost info") - wa.syncAltGhostWithInfo(ctx, jid, userInfo) + wa.syncAltGhostWithInfo(ctx, jid, ghost) } go wa.syncRemoteProfile(ctx, ghost) } diff --git a/pkg/connector/lidmigrate.go b/pkg/connector/lidmigrate.go index cfab099..ec1c700 100644 --- a/pkg/connector/lidmigrate.go +++ b/pkg/connector/lidmigrate.go @@ -210,3 +210,60 @@ func (wa *WhatsAppConnector) migrateToLIDDMs(ctx context.Context) error { wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_migrated", "true") return nil } + +func (wa *WhatsAppConnector) syncMismatchingGhosts(ctx context.Context) error { + if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_avatars_resynced") == "true" { + return nil + } + const findMismatchingGhosts = ` + SELECT id + FROM ghost + WHERE bridge_id=$1 + AND id LIKE 'lid-%' + AND avatar_mxc='' + AND EXISTS ( + SELECT pnghost.avatar_mxc + FROM ghost pnghost + WHERE pnghost.bridge_id=$1 + AND pnghost.id=(SELECT pn FROM whatsmeow_lid_map WHERE lid=replace(ghost.id, 'lid-', '')) + AND pnghost.avatar_mxc<>'' + ) + ` + var scanGhostID = dbutil.ConvertRowFn[networkid.UserID](dbutil.ScanSingleColumn[networkid.UserID]) + ghostIDs, err := scanGhostID.NewRowIter(wa.Bridge.DB.Query(ctx, findMismatchingGhosts, wa.Bridge.ID)).AsList() + if err != nil { + return fmt.Errorf("failed to get mismatching ghosts: %w", err) + } + for _, ghostID := range ghostIDs { + lid := waid.ParseUserID(ghostID) + pn, err := wa.DeviceStore.LIDMap.GetPNForLID(ctx, lid) + if err != nil { + return fmt.Errorf("failed to get PN for LID %s: %w", lid, err) + } else if pn.IsEmpty() { + zerolog.Ctx(ctx).Warn().Stringer("lid", lid).Msg("No PN for LID") + continue + } + pnGhost, err := wa.Bridge.GetGhostByID(ctx, waid.MakeUserID(pn)) + if err != nil { + return fmt.Errorf("failed to get PN ghost for %s: %w", pn, err) + } + lidGhost, err := wa.Bridge.GetGhostByID(ctx, ghostID) + if err != nil { + return fmt.Errorf("failed to get LID ghost for %s: %w", ghostID, err) + } + if lidGhost.AvatarMXC != "" || pnGhost.AvatarMXC == "" { + continue + } + zerolog.Ctx(ctx).Debug(). + Stringer("pn", pn). + Stringer("lid", lid). + Str("pn_ghost_avatar", string(pnGhost.AvatarMXC)). + Str("lid_ghost_avatar", string(lidGhost.AvatarMXC)). + Str("pn_ghost_name", pnGhost.Name). + Str("lid_ghost_name", lidGhost.Name). + Msg("Updating LID ghost avatar") + lidGhost.UpdateInfo(ctx, makeInfoFromGhost(pnGhost)) + } + wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_avatars_resynced", "true") + return nil +} diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index 05d6c40..c78b0a8 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -171,7 +171,7 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID continue } ghost.UpdateInfo(ctx, userInfo) - wa.syncAltGhostWithInfo(ctx, jid, userInfo) + wa.syncAltGhostWithInfo(ctx, jid, ghost) } } @@ -433,12 +433,12 @@ func (wa *WhatsAppClient) resyncContacts(forceAvatarSync, automatic bool) { } else { userInfo := wa.contactToUserInfo(ctx, jid, contact, "", forceAvatarSync || ghost.AvatarID == "") ghost.UpdateInfo(ctx, userInfo) - wa.syncAltGhostWithInfo(ctx, jid, userInfo) + wa.syncAltGhostWithInfo(ctx, jid, ghost) } } } -func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JID, info *bridgev2.UserInfo) { +func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JID, mainGhost *bridgev2.Ghost) { log := zerolog.Ctx(ctx) var altJID types.JID var err error @@ -463,10 +463,25 @@ func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JI Msg("Failed to get ghost for alternate JID") return } - ghost.UpdateInfo(ctx, info) + ghost.UpdateInfo(ctx, makeInfoFromGhost(mainGhost)) log.Debug(). Stringer("jid", jid). Stringer("alternate_jid", altJID). Msg("Synced alternate ghost with info") go wa.syncRemoteProfile(ctx, ghost) } + +func makeInfoFromGhost(ghost *bridgev2.Ghost) *bridgev2.UserInfo { + return &bridgev2.UserInfo{ + Identifiers: ghost.Identifiers, + Name: &ghost.Name, + Avatar: &bridgev2.Avatar{ + ID: ghost.AvatarID, + Remove: ghost.AvatarID == "" || ghost.AvatarMXC == "", + MXC: ghost.AvatarMXC, + Hash: ghost.AvatarHash, + }, + IsBot: &ghost.IsBot, + ExtraProfile: ghost.ExtraProfile, + } +} From a19ef78dd33af9d6a520a58c549bf5e01d46a73b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 8 Sep 2026 15:03:18 +0300 Subject: [PATCH 046/139] directmedia: don't allow parallel downloads of the same avatar --- pkg/connector/client.go | 1 + pkg/connector/directmedia.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index f0a5947..8c302e3 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -116,6 +116,7 @@ type WhatsAppClient struct { nextResync time.Time directMediaRetries map[networkid.MessageID]*directMediaRetry directMediaLock sync.Mutex + avatarLock exsync.KeyedMutex[types.JID] mediaRetryLock *semaphore.Weighted offlineSyncWaiter atomic.Pointer[chan error] isNewLogin bool diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index 092ce3e..8ed94ce 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -85,6 +85,8 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars if waClient.Client == nil { return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin) } + waClient.avatarLock.Lock(parsedID.Avatar.TargetJID) + defer waClient.avatarLock.Unlock(parsedID.Avatar.TargetJID) cachedInfo, err := wa.DB.AvatarCache.Get(ctx, parsedID.Avatar.TargetJID, parsedID.Avatar.AvatarID) if err != nil { return nil, fmt.Errorf("failed to get avatar cache entry: %w", err) From 82c79d1108e7c1badbcc9b2019cb560b6daaa7a2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 8 Sep 2026 19:09:47 +0300 Subject: [PATCH 047/139] dependencies: update mautrix-go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5b53985..bf892b0 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/sync v0.22.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.30.1-0.20260908102503-2f2c98079d9e + maunium.net/go/mautrix v0.30.1-0.20260908153423-501dff148280 ) require ( diff --git a/go.sum b/go.sum index 38bb208..a0ba2a8 100644 --- a/go.sum +++ b/go.sum @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.30.1-0.20260908102503-2f2c98079d9e h1:T4g/2u7ZEbAGkYGNizjYZSltWR5m0DrX5UDWqfZJdS4= -maunium.net/go/mautrix v0.30.1-0.20260908102503-2f2c98079d9e/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= +maunium.net/go/mautrix v0.30.1-0.20260908153423-501dff148280 h1:AoNwPhJCX0y93tp2nJPu/RuIt/+K+Zu4p4Q44+hKEhc= +maunium.net/go/mautrix v0.30.1-0.20260908153423-501dff148280/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= From 9ea41dcb0efa00597230cb17da42b01463c82700 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 9 Sep 2026 12:40:38 +0300 Subject: [PATCH 048/139] login: add support for adv rotation --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/login.go | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bf892b0..1c7be5c 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( 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-20260908082135-57796d3d6b41 + go.mau.fi/whatsmeow v0.0.0-20260909093947-9ec8f76db5f1 golang.org/x/image v0.45.0 golang.org/x/net v0.58.0 golang.org/x/sync v0.22.0 diff --git a/go.sum b/go.sum index a0ba2a8..1c70cc4 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbs 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-20260908082135-57796d3d6b41 h1:EUpS1/OiThY4/S1jgG5jGpC7bILOtM/Q1r1b7BI9NrM= -go.mau.fi/whatsmeow v0.0.0-20260908082135-57796d3d6b41/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= +go.mau.fi/whatsmeow v0.0.0-20260909093947-9ec8f76db5f1 h1:4EyoinbJqFq4qozxem5nklT8+pI4OaokUuG+ayE3nKQ= +go.mau.fi/whatsmeow v0.0.0-20260909093947-9ec8f76db5f1/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= 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= diff --git a/pkg/connector/login.go b/pkg/connector/login.go index a62b294..80a6abb 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "strings" "sync/atomic" "time" @@ -98,6 +99,7 @@ func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User, LoginComplete: exsync.NewEvent(), PasskeyRequest: exsync.NewEvent(), PasskeyConfirmation: exsync.NewEvent(), + ADVRotate: exsync.NewEvent(), Received515: exsync.NewEvent(), }, nil } @@ -124,6 +126,8 @@ type WALogin struct { PasskeyConfirmation *exsync.Event PasskeyConfirmationData *events.PairPasskeyConfirmation + ADVRotate *exsync.Event + Closed atomic.Bool EventHandlerID uint32 } @@ -270,6 +274,12 @@ func (wl *WALogin) handleEvent(rawEvt any) { wl.StartTime = time.Now() wl.WaitForQRs.Set() return + case *events.RotateADVSecret: + wl.Log.Debug().Msg("Rotating ADV secret in all QRs") + for i, code := range wl.QRs { + wl.QRs[i] = strings.Replace(code, evt.OldSecret, evt.NewSecret, 1) + } + wl.ADVRotate.Set() case *events.QRScannedWithoutMultidevice: wl.Log.Error().Msg("QR code scanned without multidevice enabled") wl.LoginError = ErrLoginMultideviceNotEnabled @@ -332,6 +342,7 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { Int("current_index", currentIndex) if currentIndex > prevIndex { logEvt.Msg("Returning new QR immediately") + wl.ADVRotate.Clear() wl.PrevQRIndex.Store(int32(currentIndex)) return makeQRStep(wl.QRs[currentIndex]), nil } @@ -343,6 +354,7 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { wl.Cancel() return nil, ErrLoginTimeout } + wl.ADVRotate.Clear() wl.PrevQRIndex.Store(int32(nextIndex)) return makeQRStep(wl.QRs[nextIndex]), nil case <-ctx.Done(): @@ -350,6 +362,10 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { return nil, ctx.Err() case <-wl.PasskeyRequest.GetChan(): return wl.makePasskeyStep() + case <-wl.ADVRotate.GetChan(): + wl.Log.Debug().Msg("ADV secret was rotated, returning new QR immediately") + wl.ADVRotate.Clear() + return makeQRStep(wl.QRs[nextIndex]), nil case <-wl.LoginComplete.GetChan(): // continue } From 79164b22ad498b3de829205d5e112f0e817f28a6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 10 Sep 2026 10:42:06 +0300 Subject: [PATCH 049/139] lidmigrate: resync ghost avatars in background --- pkg/connector/connector.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 3602e93..d7261d3 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -164,10 +164,12 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to migrate to LID DMs: %w", err) } - err = wa.syncMismatchingGhosts(ctx) - if err != nil { - return fmt.Errorf("failed to sync mismatching ghosts: %w", err) - } + go func() { + err = wa.syncMismatchingGhosts(wa.Bridge.BackgroundCtx) + if err != nil { + wa.Bridge.Log.Err(err).Msg("Failed to sync mismatching ghosts") + } + }() return nil } From 18ae384c98866536bb3db90d6c56ba016227848a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 11 Sep 2026 19:03:14 +0300 Subject: [PATCH 050/139] .github: make contributing.md more visible --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..764f753 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +See From 5d3fce8eff7e5079e24a924d83663e0f4bddec2f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 11 Sep 2026 19:42:24 +0300 Subject: [PATCH 051/139] handlewhatsapp: ignore sent broadcasts --- pkg/connector/chatinfo.go | 4 +++- pkg/connector/handlewhatsapp.go | 7 +++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 675a5ce..267200c 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -29,6 +29,8 @@ func (wa *WhatsAppClient) GetChatInfo(ctx context.Context, portal *bridgev2.Port return wa.getChatInfo(ctx, portalJID, nil, portal.MXID == "") } +var ErrBroadcastList = errors.New("broadcast list bridging is currently not supported") + func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation, isNew bool) (wrapped *bridgev2.ChatInfo, err error) { switch portalJID.Server { case types.DefaultUserServer, types.HiddenUserServer, types.BotServer: @@ -37,7 +39,7 @@ func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, if portalJID == types.StatusBroadcastJID { wrapped = wa.wrapStatusBroadcastInfo(ctx) } else { - return nil, fmt.Errorf("broadcast list bridging is currently not supported") + return nil, ErrBroadcastList } case types.GroupServer: info, err := wa.Client.GetGroupInfo(ctx, portalJID) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index d8df69f..ed36dca 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -284,10 +284,9 @@ func (wa *WhatsAppClient) ensureAltJIDs(ctx context.Context, info *types.Message func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) { success = true - if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast { - return - } - if evt.Info.Chat.Server == types.NewsletterServer && wa.disableNewsletter { + if (evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast) || + (evt.Info.Chat.Server == types.NewsletterServer && wa.disableNewsletter) || + (evt.Info.Chat.Server == types.BroadcastServer && evt.Info.IsFromMe) { return } if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { From d6bfdeb477dc1cd4d7d33717b860a268624211c4 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 11 Sep 2026 21:33:53 +0300 Subject: [PATCH 052/139] dependencies: update mautrix-go --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/client.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 1c7be5c..692c5ab 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/sync v0.22.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.30.1-0.20260908153423-501dff148280 + maunium.net/go/mautrix v0.30.1-0.20260911182621-dc9b2d7a77fb ) require ( diff --git a/go.sum b/go.sum index 1c70cc4..3634f94 100644 --- a/go.sum +++ b/go.sum @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.30.1-0.20260908153423-501dff148280 h1:AoNwPhJCX0y93tp2nJPu/RuIt/+K+Zu4p4Q44+hKEhc= -maunium.net/go/mautrix v0.30.1-0.20260908153423-501dff148280/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= +maunium.net/go/mautrix v0.30.1-0.20260911182621-dc9b2d7a77fb h1:QimfAexEmbycZEYon7vv11k8jgb7SYCDrsaniDNETLU= +maunium.net/go/mautrix v0.30.1-0.20260911182621-dc9b2d7a77fb/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 8c302e3..a909946 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -78,7 +78,7 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. w.Client = whatsmeow.NewClient(w.Device, waLog.Zerolog(log)) w.Client.AddEventHandlerWithSuccessStatus(w.handleWAEvent) w.Client.SynchronousAck = true - w.Client.EnableDecryptedEventBuffer = bridgev2.PortalEventBuffer == 0 + w.Client.EnableDecryptedEventBuffer = wa.Bridge.Config.PortalEventBuffer == 0 w.Client.ManualHistorySyncDownload = true w.Client.SendReportingTokens = true w.Client.AutomaticMessageRerequestFromPhone = true From 037423f0b2eb93019e52da8ada6c96e8a40e8955 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 14 Sep 2026 15:34:54 +0300 Subject: [PATCH 053/139] dependencies: update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 692c5ab..a8390bf 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( 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-20260909093947-9ec8f76db5f1 + go.mau.fi/whatsmeow v0.0.0-20260914123426-9120a3fc6159 golang.org/x/image v0.45.0 golang.org/x/net v0.58.0 golang.org/x/sync v0.22.0 diff --git a/go.sum b/go.sum index 3634f94..7124070 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbs 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-20260909093947-9ec8f76db5f1 h1:4EyoinbJqFq4qozxem5nklT8+pI4OaokUuG+ayE3nKQ= -go.mau.fi/whatsmeow v0.0.0-20260909093947-9ec8f76db5f1/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= +go.mau.fi/whatsmeow v0.0.0-20260914123426-9120a3fc6159 h1:M23L3U7EL7tDIIVgNvp1XV4wU+sKKjBmFkZ7DBNEzdg= +go.mau.fi/whatsmeow v0.0.0-20260914123426-9120a3fc6159/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= 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= From 991c12cdab59416ce98271f0ef983ecee04ce68e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 14 Sep 2026 15:35:08 +0300 Subject: [PATCH 054/139] changelog: update --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d17865..b6567d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# v26.09 (unreleased) + +* Bumped minimum Go version to 1.26. +* Added option to limit eager member sync in group chat portals. +* Added option to fetch avatars lazily when using direct media. +* Added one-time migration to ensure LID ghost avatars are consistent with the + old phone number ghosts. +* Fixed the resolve identifier command and API not returning any user info for + users who hadn't previously been encountered. +* Fixed group portal power levels to always allow poll responses. +* Fixed handling history sync events where the phone sends nonsensical + timestamps. +* Fixed handling edits to HD media captions. +* Fixed handling bridging own read receipts from the native apps in channels. + # v26.08 * Switched direct chats to use LIDs instead of phone numbers. From c1e30935dc3fb407d6c0a8bd778b64175949db5e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 14 Sep 2026 18:07:29 +0300 Subject: [PATCH 055/139] dependencies: update whatsmeow again --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a8390bf..83f457f 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( 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-20260914123426-9120a3fc6159 + go.mau.fi/whatsmeow v0.0.0-20260914150520-0d3b644136bf golang.org/x/image v0.45.0 golang.org/x/net v0.58.0 golang.org/x/sync v0.22.0 diff --git a/go.sum b/go.sum index 7124070..d56e1c4 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbs 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-20260914123426-9120a3fc6159 h1:M23L3U7EL7tDIIVgNvp1XV4wU+sKKjBmFkZ7DBNEzdg= -go.mau.fi/whatsmeow v0.0.0-20260914123426-9120a3fc6159/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= +go.mau.fi/whatsmeow v0.0.0-20260914150520-0d3b644136bf h1:9iToTs8b6MMX8lWSryybzR9sqYGYG4/LpXj+cy0UC4k= +go.mau.fi/whatsmeow v0.0.0-20260914150520-0d3b644136bf/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= 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= From a76341f281c7b6f1ac3913a2a47bd309f082b5ab Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 14 Sep 2026 18:17:24 +0300 Subject: [PATCH 056/139] handlewhatsapp: log unknown protocol message types --- pkg/connector/handlewhatsapp.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index ed36dca..5381311 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -334,7 +334,15 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa wa.Main.Bridge.Config.Backfill.Enabled { wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification) } - if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") { + if parsedMessageType == "ignore" { + return + } else if strings.HasPrefix(parsedMessageType, "unknown_protocol_") { + wa.UserLogin.Log.Debug(). + Str("message_id", evt.Info.ID). + Stringer("chat_jid", evt.Info.Chat). + Stringer("sender_jid", evt.Info.Sender). + Stringer("protocol_message_type", evt.Message.GetProtocolMessage().GetType()). + Msg("Ignoring unknown protocol message") return } From 1ea9f02ade4b7c5c2cc058199912c5ce9c5189b0 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 14 Sep 2026 19:28:43 +0300 Subject: [PATCH 057/139] msgconv/from-whatsapp: bridge advanced rich responses as unknown message types --- pkg/msgconv/from-whatsapp.go | 2 +- pkg/msgconv/wa-misc.go | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go index e912732..6dac5a2 100644 --- a/pkg/msgconv/from-whatsapp.go +++ b/pkg/msgconv/from-whatsapp.go @@ -190,7 +190,7 @@ func (mc *MessageConverter) ToMatrix( case waMsg.KeepInChatMessage != nil: part, contextInfo = mc.convertKeepInChatMessage(ctx, waMsg.KeepInChatMessage) case waMsg.RichResponseMessage != nil: - part, contextInfo = mc.convertRichResponseMessage(ctx, waMsg.RichResponseMessage) + part, contextInfo = mc.convertRichResponseMessage(ctx, waMsg.RichResponseMessage, waMsg) case waMsg.ImageMessage != nil: part, contextInfo = mc.convertMediaMessage(ctx, waMsg.ImageMessage, "photo", info, isViewOnce, previouslyConvertedPart) case waMsg.StickerMessage != nil: diff --git a/pkg/msgconv/wa-misc.go b/pkg/msgconv/wa-misc.go index fa495e0..be5b60f 100644 --- a/pkg/msgconv/wa-misc.go +++ b/pkg/msgconv/wa-misc.go @@ -351,7 +351,7 @@ func (mc *MessageConverter) convertKeepInChatMessage(ctx context.Context, msg *w }, nil } -func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { +func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage, fullMsg *waE2E.Message) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { var body strings.Builder // TODO switch to new format? @@ -363,10 +363,14 @@ func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg body.WriteString(submsg.GetMessageText()) } } - + unknownPart, _ := mc.convertUnknownMessage(ctx, fullMsg) + if body.Len() == 0 { + return unknownPart, msg.GetContextInfo() + } content := format.RenderMarkdown(body.String(), true, false) return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, Content: &content, + Extra: unknownPart.Extra, }, msg.GetContextInfo() } From 741f45d6497abb18bbcf9bb93106ddc3b9b5aa68 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 16 Sep 2026 00:22:26 +0300 Subject: [PATCH 058/139] dependencies: update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 83f457f..ccd3fce 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( 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-20260914150520-0d3b644136bf + go.mau.fi/whatsmeow v0.0.0-20260915211301-f376da267f95 golang.org/x/image v0.45.0 golang.org/x/net v0.58.0 golang.org/x/sync v0.22.0 diff --git a/go.sum b/go.sum index d56e1c4..5c05090 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbs 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-20260914150520-0d3b644136bf h1:9iToTs8b6MMX8lWSryybzR9sqYGYG4/LpXj+cy0UC4k= -go.mau.fi/whatsmeow v0.0.0-20260914150520-0d3b644136bf/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= +go.mau.fi/whatsmeow v0.0.0-20260915211301-f376da267f95 h1:xwn1RBsgFKCFDTLKpsdrpfgZoLX6wlfHKRBpGjnpkVI= +go.mau.fi/whatsmeow v0.0.0-20260915211301-f376da267f95/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= 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= From 9361103b64f5718ffd03f76bc40cb436f30c9efd Mon Sep 17 00:00:00 2001 From: Toni Spets Date: Wed, 16 Sep 2026 12:00:30 +0300 Subject: [PATCH 059/139] backfill: don't panic when the whatsmeow client is nil (#954) --- pkg/connector/backfill.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index cab460f..e0f5ffc 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -602,6 +602,9 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( messages []*waWeb.WebMessageInfo, explodeOnError bool, ) (*bridgev2.FetchMessagesResponse, error) { + if wa.Client == nil { + return nil, bridgev2.ErrNotLoggedIn + } oldestTS := messages[len(messages)-1].GetMessageTimestamp() newestTS := messages[0].GetMessageTimestamp() convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages)) From 52e75964a0f03a0b3e21e658a2fd7b345dff40b4 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 16 Sep 2026 13:14:00 +0300 Subject: [PATCH 060/139] Bump version to v26.09 --- CHANGELOG.md | 5 ++- cmd/mautrix-whatsapp/main.go | 2 +- go.mod | 28 ++++++++--------- go.sum | 60 +++++++++++++++++------------------- 4 files changed, 47 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6567d6..d5dd173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# v26.09 (unreleased) +# v26.09 * Bumped minimum Go version to 1.26. * Added option to limit eager member sync in group chat portals. @@ -12,6 +12,9 @@ timestamps. * Fixed handling edits to HD media captions. * Fixed handling bridging own read receipts from the native apps in channels. +* Fixed decrypting messages from the new Muse AI bot + * May require resyncing app state using `!wa sync appstate regular_high`. + * The message contents aren't supported yet. # v26.08 diff --git a/cmd/mautrix-whatsapp/main.go b/cmd/mautrix-whatsapp/main.go index 011e9b0..a1d85c3 100644 --- a/cmd/mautrix-whatsapp/main.go +++ b/cmd/mautrix-whatsapp/main.go @@ -18,7 +18,7 @@ var m = mxmain.BridgeMain{ Name: "mautrix-whatsapp", URL: "https://github.com/mautrix/whatsapp", Description: "A Matrix-WhatsApp puppeting bridge.", - Version: "26.08", + Version: "26.09", SemCalVer: true, Connector: &connector.WhatsAppConnector{}, } diff --git a/go.mod b/go.mod index ccd3fce..3768dca 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module go.mau.fi/mautrix-whatsapp go 1.26.0 -toolchain go1.27.0 +toolchain go1.27.1 tool go.mau.fi/util/cmd/maubuild @@ -10,15 +10,15 @@ 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/util v0.10.1 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260915211301-f376da267f95 - golang.org/x/image v0.45.0 - golang.org/x/net v0.58.0 - golang.org/x/sync v0.22.0 + go.mau.fi/whatsmeow v0.0.0-20260916100317-2375e1751bbd + golang.org/x/image v0.46.0 + golang.org/x/net v0.59.0 + golang.org/x/sync v0.23.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.30.1-0.20260911182621-dc9b2d7a77fb + maunium.net/go/mautrix v0.31.0 ) require ( @@ -31,7 +31,7 @@ require ( 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.50 // indirect + github.com/mattn/go-sqlite3 v1.14.52 // indirect github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rs/xid v1.6.0 // indirect @@ -40,14 +40,14 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect - github.com/yuin/goldmark v1.8.5 // indirect + github.com/yuin/goldmark v1.8.6 // indirect go.mau.fi/libsignal v0.2.2 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect - golang.org/x/crypto v0.55.0 // indirect - golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // 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.57.0 // indirect + golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect + golang.org/x/mod v0.41.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.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 5c05090..ac06de4 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,6 @@ github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6p github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -34,13 +32,11 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-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.50 h1:dmdFvo1XG4MPzA4IkAmE9upVz/Nj31uRoM5+jC8hYbY= -github.com/mattn/go-sqlite3 v1.14.50/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= +github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U= +github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 h1:lcWAnrqr2nNfDiArwFNHCE4787Mw2tCdVSOXCru0/0E= github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -52,8 +48,8 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= +github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= @@ -67,35 +63,35 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/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= +github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= +github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU= go.mau.fi/libsignal v0.2.2/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/util v0.10.1 h1:1oSqb4TwzLA0cUDY0aomyBPFKkZ3J5fqCmrXV3VH3GQ= +go.mau.fi/util v0.10.1/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= -go.mau.fi/whatsmeow v0.0.0-20260915211301-f376da267f95 h1:xwn1RBsgFKCFDTLKpsdrpfgZoLX6wlfHKRBpGjnpkVI= -go.mau.fi/whatsmeow v0.0.0-20260915211301-f376da267f95/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= +go.mau.fi/whatsmeow v0.0.0-20260916100317-2375e1751bbd h1:ZRMG9rK+Vghe4U+xGDGI2hkwJgoPTqf25HWm3nkBM8w= +go.mau.fi/whatsmeow v0.0.0-20260916100317-2375e1751bbd/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/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-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= -golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= -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.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= +golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ= +golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.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= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -107,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.20260911182621-dc9b2d7a77fb h1:QimfAexEmbycZEYon7vv11k8jgb7SYCDrsaniDNETLU= -maunium.net/go/mautrix v0.30.1-0.20260911182621-dc9b2d7a77fb/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= +maunium.net/go/mautrix v0.31.0 h1:x6XNBSa0kOaBgcleI+ZBLkGvBdHIE0qdeY6TZ4wWUeI= +maunium.net/go/mautrix v0.31.0/go.mod h1:vfPYtoGAjlTnIF+W8tl+rjX3yB4PEfqYR2sDXvQo4Co= From e4b680c9f31904b25bf6480108fe9f734fb4108d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 21 Sep 2026 15:32:31 +0300 Subject: [PATCH 061/139] msgconv/from-whatsapp: add support for new rich response format --- go.mod | 6 +- go.sum | 8 +- pkg/msgconv/wa-misc.go | 5 +- pkg/msgconv/wa-rich-response.go | 177 ++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 pkg/msgconv/wa-rich-response.go diff --git a/go.mod b/go.mod index 3768dca..fc81749 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,10 @@ 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 + github.com/yuin/goldmark v1.8.6 + go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260916100317-2375e1751bbd + go.mau.fi/whatsmeow v0.0.0-20260921121126-35ae40906e74 golang.org/x/image v0.46.0 golang.org/x/net v0.59.0 golang.org/x/sync v0.23.0 @@ -40,7 +41,6 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect - github.com/yuin/goldmark v1.8.6 // indirect go.mau.fi/libsignal v0.2.2 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect golang.org/x/crypto v0.57.0 // indirect diff --git a/go.sum b/go.sum index ac06de4..4a39eb4 100644 --- a/go.sum +++ b/go.sum @@ -67,12 +67,12 @@ github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU= go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0= -go.mau.fi/util v0.10.1 h1:1oSqb4TwzLA0cUDY0aomyBPFKkZ3J5fqCmrXV3VH3GQ= -go.mau.fi/util v0.10.1/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= +go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcUkqKshE+n239UtbbJOn4= +go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= 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-20260916100317-2375e1751bbd h1:ZRMG9rK+Vghe4U+xGDGI2hkwJgoPTqf25HWm3nkBM8w= -go.mau.fi/whatsmeow v0.0.0-20260916100317-2375e1751bbd/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= +go.mau.fi/whatsmeow v0.0.0-20260921121126-35ae40906e74 h1:z3cItqSX0u+rsM6/81MB1I4bBa92jPAf6QZAvmGc0L0= +go.mau.fi/whatsmeow v0.0.0-20260921121126-35ae40906e74/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= 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.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= diff --git a/pkg/msgconv/wa-misc.go b/pkg/msgconv/wa-misc.go index be5b60f..cda33c5 100644 --- a/pkg/msgconv/wa-misc.go +++ b/pkg/msgconv/wa-misc.go @@ -352,9 +352,10 @@ func (mc *MessageConverter) convertKeepInChatMessage(ctx context.Context, msg *w } func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage, fullMsg *waE2E.Message) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { + if msg.GetUnifiedResponse() != nil { + return mc.convertUnifiedRichResponseMessage(ctx, msg.GetUnifiedResponse().GetData()), msg.GetContextInfo() + } var body strings.Builder - - // TODO switch to new format? for i, submsg := range msg.GetSubmessages() { if submsg.GetMessageType() == waAICommonDeprecated.AIRichResponseSubMessageType_AI_RICH_RESPONSE_TEXT { if i > 0 { diff --git a/pkg/msgconv/wa-rich-response.go b/pkg/msgconv/wa-rich-response.go new file mode 100644 index 0000000..d2dab51 --- /dev/null +++ b/pkg/msgconv/wa-rich-response.go @@ -0,0 +1,177 @@ +// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2026 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package msgconv + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "html" + "strings" + + "github.com/rs/zerolog" + "github.com/yuin/goldmark" + "go.mau.fi/whatsmeow/types/richresponse" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/format/mdext" +) + +func (mc *MessageConverter) convertUnifiedRichResponseMessage(ctx context.Context, data []byte) *bridgev2.ConvertedMessagePart { + extra := map[string]any{} + if json.Valid(data) && len(data) < 24*1024 { + extra["fi.mau.whatsapp.rich_message"] = json.RawMessage(data) + } else if len(data) < 16*1024 { + extra["fi.mau.whatsapp.non_json_rich_message"] = base64.StdEncoding.EncodeToString(data) + } + var rrMsg richresponse.RichResponse + err := json.Unmarshal(data, &rrMsg) + if err != nil || len(rrMsg.Sections) == 0 { + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to unmarshal unified rich response message") + } + return &bridgev2.ConvertedMessagePart{ + Type: event.EventMessage, + Content: &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: "Unknown message type, please view it on the WhatsApp app", + }, + Extra: extra, + } + } + extra["fi.mau.whatsapp.rich_response_id"] = rrMsg.ResponseID + var htmlBuf strings.Builder + for _, mv := range rrMsg.Sections { + for _, p := range mv.Model.GetPrimitives() { + mc.convertRichResponsePrimitive(ctx, p, &htmlBuf) + } + } + return &bridgev2.ConvertedMessagePart{ + Type: event.EventMessage, + Content: new(format.HTMLToContent(htmlBuf.String())), + Extra: extra, + } +} + +var mdRender = goldmark.New(format.Extensions, format.HTMLOptions, goldmark.WithExtensions(mdext.EscapeHTML)) + +func (mc *MessageConverter) convertRichMarkdownText(ctx context.Context, text *richresponse.GenAIMarkdownTextUXPrimitive, buf *strings.Builder) error { + for _, ent := range text.InlineEntities { + switch meta := ent.Metadata.(type) { + //case *richresponse.GenAISearchCitationItem: + case *richresponse.GenAIInlineLinkItem: + text.Text = strings.ReplaceAll( + text.Text, + fmt.Sprintf("{{%s}}", ent.Key), + fmt.Sprintf("[%s](%s)", meta.DisplayName, meta.URL), + ) + case *richresponse.GenAIDeepLinkItem: + text.Text = strings.ReplaceAll( + text.Text, + fmt.Sprintf("{{%s}}", ent.Key), + meta.Text, + ) + case *richresponse.GenAILatexItem: + text.Text = strings.ReplaceAll( + text.Text, + fmt.Sprintf("{{%s}}", ent.Key), + fmt.Sprintf(`%[1]s`, html.EscapeString(meta.LatexExpression)), + ) + default: + text.Text = strings.ReplaceAll( + text.Text, + fmt.Sprintf("{{%s}}", ent.Key), + ent.Key, + ) + } + } + return mdRender.Convert([]byte(text.Text), buf) +} + +func (mc *MessageConverter) convertRichResponsePrimitive(ctx context.Context, primitive richresponse.Primitive, buf *strings.Builder) error { + switch p := primitive.(type) { + case *richresponse.GenAICodeUXPrimitive: + if p.Language != "" { + _, _ = fmt.Fprintf(buf, `
`, html.EscapeString(p.Language))
+		} else {
+			buf.WriteString("
")
+		}
+		for _, part := range p.CodeBlocks {
+			buf.WriteString(part.Content)
+		}
+		buf.WriteString("
") + case *richresponse.GenAIMarkdownTextUXPrimitive: + return mc.convertRichMarkdownText(ctx, p, buf) + case *richresponse.GenATableUXPrimitive: + buf.WriteString("") + for _, row := range p.Rows { + buf.WriteString("") + cellType := "tr" + if row.IsHeader { + cellType = "th" + } + if len(row.MarkdownCells) > 0 { + for _, cell := range row.MarkdownCells { + _, _ = fmt.Fprintf(buf, "<%s>", cellType) + err := mc.convertRichMarkdownText(ctx, &cell, buf) + if err != nil { + return err + } + _, _ = fmt.Fprintf(buf, "", cellType) + } + } else { + for _, cell := range row.Cells { + _, _ = fmt.Fprintf(buf, "<%s>%s", cellType, html.EscapeString(cell), cellType) + } + } + buf.WriteString("") + } + buf.WriteString("
") + case *richresponse.GenAILatexUXPrimitive: + _, _ = fmt.Fprintf(buf, `%[1]s`, html.EscapeString(p.GetLatexExpression())) + case *richresponse.FOATextPrimitive: + // TODO remove placeholders? + return mdRender.Convert([]byte(p.Text), buf) + case *richresponse.GenAIMetadataTextPrimitive: + return mdRender.Convert([]byte(p.Text), buf) + //case *richresponse.GenAIBotThinkingStatusPrimitive: + //case *richresponse.GenAIProductItemCardPrimitive: + //case *richresponse.GenAIImagePrimitive: + //case *richresponse.GenAITaskPrimitive: + //case *richresponse.GenAIReelPrimitive: + //case *richresponse.GenAIPostPrimitive: + //case *richresponse.GenAIImaginePrimitive: + //case *richresponse.GenAISearchResultPrimitive: + //case *richresponse.FOABloksPrimitive: + case *richresponse.GenAIDividerPrimitive: + // TODO dots? + buf.WriteString("
") + case *richresponse.GenAISpacerPrimitive: + if p.Spacing <= 1 { + buf.WriteString("
") + } else { + for range p.Spacing { + buf.WriteString("
") + } + } + default: + buf.WriteString("

Unknown primitive type

") + } + return nil +} From 85aa4a6d0d2a2a34954231f30f9c9939d6b0d47e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 21 Sep 2026 15:56:51 +0300 Subject: [PATCH 062/139] msgconv/from-whatsapp: cut off too long messages --- go.mod | 2 +- go.sum | 4 ++-- pkg/msgconv/wa-text.go | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fc81749..97c0a0a 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( golang.org/x/sync v0.23.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.31.0 + maunium.net/go/mautrix v0.31.1-0.20260921130557-78e83388b27e ) require ( diff --git a/go.sum b/go.sum index 4a39eb4..d7de15c 100644 --- a/go.sum +++ b/go.sum @@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.31.0 h1:x6XNBSa0kOaBgcleI+ZBLkGvBdHIE0qdeY6TZ4wWUeI= -maunium.net/go/mautrix v0.31.0/go.mod h1:vfPYtoGAjlTnIF+W8tl+rjX3yB4PEfqYR2sDXvQo4Co= +maunium.net/go/mautrix v0.31.1-0.20260921130557-78e83388b27e h1:ndauW8laSq7G105w9hPkTjwaOw5/f2z2SgMr9Yq6MKw= +maunium.net/go/mautrix v0.31.1-0.20260921130557-78e83388b27e/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk= diff --git a/pkg/msgconv/wa-text.go b/pkg/msgconv/wa-text.go index 032e3fd..7859f37 100644 --- a/pkg/msgconv/wa-text.go +++ b/pkg/msgconv/wa-text.go @@ -42,6 +42,12 @@ func (mc *MessageConverter) convertTextMessage(ctx context.Context, msg *waE2E.M } contextInfo = msg.GetExtendedTextMessage().GetContextInfo() mc.parseFormatting(part.Content, false, false) + if len(part.Content.FormattedBody) > 20000 { + part.Content.Body = part.Content.Body[:500] + "[...]" + } + if len(part.Content.FormattedBody) > 40000 { + part.Content.FormattedBody = part.Content.FormattedBody[:40000] + "
[long message cut off]" + } part.Content.BeeperLinkPreviews = mc.convertURLPreviewToBeeper(ctx, msg.GetExtendedTextMessage()) return } From 52f0c60aa60efd39fce13fa65113f026f50db43a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 24 Sep 2026 15:04:51 +0300 Subject: [PATCH 063/139] handlewhatsapp: dispatch unavailable resend responses directly as edits --- pkg/connector/events.go | 5 ++++- pkg/connector/handlewhatsapp.go | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/pkg/connector/events.go b/pkg/connector/events.go index 1f4e118..c23189d 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -315,7 +315,10 @@ func (evt *WANowDecryptableMessage) GetTargetMessage() networkid.MessageID { } func (evt *WANowDecryptableMessage) AddLogContext(c zerolog.Context) zerolog.Context { - return c + if evt.editParts != nil { + return c.Bool("from_upsert", true) + } + return evt.WAMessageEvent.AddLogContext(c.Bool("from_upsert", false)) } func (evt *WANowDecryptableMessage) GetType() bridgev2.RemoteEventType { diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 5381311..dba4f1e 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -371,7 +371,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa return } - res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{ + wrappedEvt := &WAMessageEvent{ MessageInfoWrapper: &MessageInfoWrapper{ Info: evt.Info, wa: wa, @@ -381,8 +381,20 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa parsedMessageType: parsedMessageType, dontRenderEdited: dontRenderEdited, - }) - return res.Success + } + if evt.UnavailableRequestID != "" { + wa.UserLogin.Log.Debug(). + Str("message_id", evt.Info.ID). + Str("unavailable_request_id", evt.UnavailableRequestID). + Msg("Received placeholder resend response") + wa.trackUndecryptableResolved(evt) + wrappedEvt.isUndecryptableUpsertSubEvent = true + // Dispatch directly as edit instead of the usual upsert, so that the message doesn't + // get re-bridged if it was deleted before the resend response was received. + return wa.UserLogin.QueueRemoteEvent(&WANowDecryptableMessage{WAMessageEvent: wrappedEvt}).Success + } + + return wa.UserLogin.QueueRemoteEvent(wrappedEvt).Success } func makeHDMediaReplacementEdit(message *waE2E.Message, parentKey *waCommon.MessageKey) (*waE2E.ProtocolMessage, bool) { From 173f18405c619fa69708c54ec72bb5c64bd031f8 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 25 Sep 2026 19:27:23 +0300 Subject: [PATCH 064/139] login: wait for otk upload before confirming login success --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/client.go | 10 ++++++++-- pkg/connector/login.go | 10 ++++++++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 97c0a0a..6f12b2a 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/yuin/goldmark v1.8.6 go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260921121126-35ae40906e74 + go.mau.fi/whatsmeow v0.0.0-20260925162019-b3832c2bd1d1 golang.org/x/image v0.46.0 golang.org/x/net v0.59.0 golang.org/x/sync v0.23.0 diff --git a/go.sum b/go.sum index d7de15c..7e65787 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcU go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= 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-20260921121126-35ae40906e74 h1:z3cItqSX0u+rsM6/81MB1I4bBa92jPAf6QZAvmGc0L0= -go.mau.fi/whatsmeow v0.0.0-20260921121126-35ae40906e74/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= +go.mau.fi/whatsmeow v0.0.0-20260925162019-b3832c2bd1d1 h1:iGMEoW98F/m73hSf4+EwV8l5yVfsZGFqvYRM7dAMWs4= +go.mau.fi/whatsmeow v0.0.0-20260925162019-b3832c2bd1d1/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= 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.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= diff --git a/pkg/connector/client.go b/pkg/connector/client.go index a909946..2287fea 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -206,13 +206,17 @@ func (wa *WhatsAppClient) GetLID() types.JID { } func (wa *WhatsAppClient) Connect(ctx context.Context) { + wa.connect(ctx) +} + +func (wa *WhatsAppClient) connect(ctx context.Context) bool { if wa.Client == nil { state := status.BridgeState{ StateEvent: status.StateBadCredentials, Error: WANotLoggedIn, } wa.UserLogin.BridgeState.Send(state) - return + return false } wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting}) wa.Main.firstClientConnectOnce.Do(wa.Main.onFirstClientConnect) @@ -220,7 +224,7 @@ func (wa *WhatsAppClient) Connect(ctx context.Context) { zerolog.Ctx(ctx).Err(err).Msg("Failed to update proxy") } if ctx.Err() != nil { - return + return false } wa.initMC() wa.startLoops() @@ -237,7 +241,9 @@ func (wa *WhatsAppClient) Connect(ctx context.Context) { }, } wa.UserLogin.BridgeState.Send(state) + return false } + return true } func (wa *WhatsAppClient) notifyOfflineSyncWaiter(err error) { diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 80a6abb..fa2c479 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -410,8 +410,14 @@ func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, er return nil, fmt.Errorf("failed to create user login: %w", err) } - ul.Client.(*WhatsAppClient).isNewLogin = true - ul.Client.Connect(ul.Log.WithContext(wl.Main.Bridge.BackgroundCtx)) + c := ul.Client.(*WhatsAppClient) + c.isNewLogin = true + if c.connect(ul.Log.WithContext(wl.Main.Bridge.BackgroundCtx)) { + err = c.Client.PreKeysUploaded.WaitTimeoutCtx(ctx, 5*time.Second) + if err != nil { + wl.Log.Warn().Err(err).Msg("Prekey upload wait failed") + } + } return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeComplete, From 054064eddb27405fe353f0a6dfc75a00e4f93982 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 27 Sep 2026 18:04:19 +0300 Subject: [PATCH 065/139] backfill,msgconv: handle panics in on-demand history syncs --- pkg/connector/backfill.go | 32 +++++++++++++++++++++++++++++--- pkg/msgconv/from-matrix.go | 2 +- pkg/msgconv/from-whatsapp.go | 14 ++++++++++++-- pkg/msgconv/matrixpoll.go | 6 +++--- pkg/msgconv/wa-poll.go | 6 +++--- 5 files changed, 48 insertions(+), 12 deletions(-) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index e0f5ffc..50a0d4c 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "runtime/debug" "slices" "strconv" "strings" @@ -12,6 +13,7 @@ import ( "time" "github.com/rs/zerolog" + "go.mau.fi/util/exerrors" "go.mau.fi/util/exmaps" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" @@ -62,7 +64,7 @@ func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) { } }() for { - var resetTimer bool + var resetTimer, errored bool select { case <-wa.historySyncWakeup: dispatchTimer.Stop() @@ -72,12 +74,21 @@ func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) { } else if notif == nil { wa.UserLogin.Log.Debug().Msg("No more queued history sync notifications") } else { - resetTimer = wa.downloadAndSaveWAHistorySyncData(ctx, notif, rowid) + resetTimer, errored = wa.downloadAndSaveWAHistorySyncData(ctx, notif, rowid) // Continue waking up the loop until all queued notifications are processed select { case wa.historySyncWakeup <- struct{}{}: default: } + if errored { + wa.UserLogin.Log.Debug().Msg("Temporarily stopping history sync loop after error") + select { + case <-time.After(1 * time.Minute): + case <-ctx.Done(): + wa.UserLogin.Log.Debug().Msg("Stopping main history sync loop") + return + } + } } case <-ctx.Done(): wa.UserLogin.Log.Debug().Msg("Stopping main history sync loop") @@ -109,13 +120,21 @@ func (wa *WhatsAppClient) saveWAHistorySyncNotification(ctx context.Context, evt } } -func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, evt *waE2E.HistorySyncNotification, rowid int) (resetTimer bool) { +func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, evt *waE2E.HistorySyncNotification, rowid int) (resetTimer, errored bool) { log := wa.UserLogin.Log.With(). Str("action", "download history sync"). Stringer("sync_type", evt.GetSyncType()). Uint32("chunk_order", evt.GetChunkOrder()). Uint32("progress", evt.GetProgress()). Logger() + defer func() { + if v := recover(); v != nil { + log.Err(exerrors.RecoverToError(v)). + Bytes(zerolog.ErrorStackFieldName, debug.Stack()). + Msg("History sync saving panicked") + errored = true + } + }() log.Debug(). Int64("oldest_msg_in_chunk_ts", evt.GetOldestMsgInChunkTimestampSec()). Any("full_request_meta", evt.GetFullHistorySyncOnDemandRequestMetadata()). @@ -367,6 +386,13 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) { log := wa.UserLogin.Log.With(). Str("action", "create portals from history sync"). Logger() + defer func() { + if v := recover(); v != nil { + log.Err(exerrors.RecoverToError(v)). + Bytes(zerolog.ErrorStackFieldName, debug.Stack()). + Msg("History sync portal creation panicked") + } + }() ctx = log.WithContext(ctx) limit := wa.Main.Config.HistorySync.MaxInitialConversations loginTS := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).LoggedInAt diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index 57f9113..b3ddcf9 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -150,7 +150,7 @@ func (mc *MessageConverter) ToWhatsApp( } lid := parsedID.Sender if lid.Server == types.DefaultUserServer { - lid, err = client.Store.LIDs.GetLIDForPN(ctx, lid) + lid, err = getStore(client).LIDs.GetLIDForPN(ctx, lid) if err != nil { return nil, nil, fmt.Errorf("failed to get LID for PN: %w", err) } diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go index 6dac5a2..99d036f 100644 --- a/pkg/msgconv/from-whatsapp.go +++ b/pkg/msgconv/from-whatsapp.go @@ -30,6 +30,7 @@ import ( "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waE2E" + "go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/types" _ "golang.org/x/image/webp" "maunium.net/go/mautrix/bridgev2" @@ -277,10 +278,10 @@ func (mc *MessageConverter) ToMatrix( var pn, lid types.JID if pcp.Server == types.DefaultUserServer { pn = pcp - lid, _ = client.Store.LIDs.GetLIDForPN(ctx, pcp) + lid, _ = getStore(client).LIDs.GetLIDForPN(ctx, pcp) } else if pcp.Server == types.HiddenUserServer { lid = pcp - pn, _ = client.Store.LIDs.GetPNForLID(ctx, pcp) + pn, _ = getStore(client).LIDs.GetPNForLID(ctx, pcp) } else if pcp.Server == types.BotServer { lid = pcp } @@ -322,3 +323,12 @@ func (mc *MessageConverter) ToMatrix( return cm } + +func getStore(cli *whatsmeow.Client) *store.Device { + if cli != nil { + if currentStore := cli.Store; currentStore != nil { + return currentStore + } + } + return store.NoopDevice +} diff --git a/pkg/msgconv/matrixpoll.go b/pkg/msgconv/matrixpoll.go index a5f02ea..5d5d014 100644 --- a/pkg/msgconv/matrixpoll.go +++ b/pkg/msgconv/matrixpoll.go @@ -150,8 +150,8 @@ func MessageIDToInfo(ctx context.Context, client *whatsmeow.Client, parsedMsgID chat := parsedMsgID.Chat sender := parsedMsgID.Sender if chat.Server == types.DefaultUserServer { - chatLID, _ := client.Store.LIDs.GetLIDForPN(ctx, chat) - senderLID, _ := client.Store.LIDs.GetLIDForPN(ctx, sender) + chatLID, _ := getStore(client).LIDs.GetLIDForPN(ctx, chat) + senderLID, _ := getStore(client).LIDs.GetLIDForPN(ctx, sender) if !chatLID.IsEmpty() && !senderLID.IsEmpty() { chat = chatLID sender = senderLID @@ -161,7 +161,7 @@ func MessageIDToInfo(ctx context.Context, client *whatsmeow.Client, parsedMsgID MessageSource: types.MessageSource{ Chat: chat, Sender: sender, - IsFromMe: sender.User == client.Store.GetLID().User || sender.User == client.Store.GetJID().User, + IsFromMe: sender.User == getStore(client).GetLID().User || sender.User == getStore(client).GetJID().User, IsGroup: chat.Server == types.GroupServer, }, ID: parsedMsgID.ID, diff --git a/pkg/msgconv/wa-poll.go b/pkg/msgconv/wa-poll.go index 881a5fb..2bcf671 100644 --- a/pkg/msgconv/wa-poll.go +++ b/pkg/msgconv/wa-poll.go @@ -108,7 +108,7 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender sender.Server = types.DefaultUserServer } } else if chat.Server == types.DefaultUserServer || chat.Server == types.HiddenUserServer || chat.Server == types.BotServer { - if sender.User == client.Store.GetJID().User || sender.User == client.Store.GetLID().User { + if sender.User == getStore(client).GetJID().User || sender.User == getStore(client).GetLID().User { // Message key is not from the sender, but message sender (containing key) is me, // so message key sender is the other user in the DM sender = chat @@ -116,9 +116,9 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender // Message key is not from the sender, but message sender (containing key) is not me, // so message key sender is me if chat.Server == types.HiddenUserServer { - sender = client.Store.GetLID().ToNonAD() + sender = getStore(client).GetLID().ToNonAD() } else { - sender = client.Store.GetJID().ToNonAD() + sender = getStore(client).GetJID().ToNonAD() } } } else { From bcd77819e3f7655c953372f10191d6109f76e9e0 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 28 Sep 2026 12:21:56 +0000 Subject: [PATCH 066/139] handlewhatsapp: use proper profile for Muse and fix message handling (#958) Co-authored-by: Indent --- go.mod | 4 +- go.sum | 8 ++-- pkg/connector/events.go | 20 +++++++++ pkg/connector/handlewhatsapp.go | 6 +++ pkg/connector/muse.go | 80 +++++++++++++++++++++++++++++++++ pkg/msgconv/wa-rich-response.go | 29 +++++++----- 6 files changed, 129 insertions(+), 18 deletions(-) create mode 100644 pkg/connector/muse.go diff --git a/go.mod b/go.mod index 6f12b2a..dcae3bb 100644 --- a/go.mod +++ b/go.mod @@ -13,13 +13,13 @@ require ( github.com/yuin/goldmark v1.8.6 go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260925162019-b3832c2bd1d1 + go.mau.fi/whatsmeow v0.0.0-20260927171547-45cfce066cd2 golang.org/x/image v0.46.0 golang.org/x/net v0.59.0 golang.org/x/sync v0.23.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.31.1-0.20260921130557-78e83388b27e + maunium.net/go/mautrix v0.31.1-0.20260928113010-0d8772dfa579 ) require ( diff --git a/go.sum b/go.sum index 7e65787..82983af 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcU go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= 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-20260925162019-b3832c2bd1d1 h1:iGMEoW98F/m73hSf4+EwV8l5yVfsZGFqvYRM7dAMWs4= -go.mau.fi/whatsmeow v0.0.0-20260925162019-b3832c2bd1d1/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= +go.mau.fi/whatsmeow v0.0.0-20260927171547-45cfce066cd2 h1:MdSiBaMjvUIFbk8Cqf90CJU0JrRYXnvGKQNoM3ciaas= +go.mau.fi/whatsmeow v0.0.0-20260927171547-45cfce066cd2/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= 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.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= @@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.31.1-0.20260921130557-78e83388b27e h1:ndauW8laSq7G105w9hPkTjwaOw5/f2z2SgMr9Yq6MKw= -maunium.net/go/mautrix v0.31.1-0.20260921130557-78e83388b27e/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk= +maunium.net/go/mautrix v0.31.1-0.20260928113010-0d8772dfa579 h1:CfIu/q0W0z8uLR9o6C0kcFQqFTTWUi2j9+Vd9yQV1xg= +maunium.net/go/mautrix v0.31.1-0.20260928113010-0d8772dfa579/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk= diff --git a/pkg/connector/events.go b/pkg/connector/events.go index c23189d..c5cdac8 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -201,6 +201,7 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por cm := evt.wa.Main.MsgConv.ToMatrix( ctx, portal, evt.wa.Client, intent, editedMsg, evt.MsgEvent.RawMessage, &evt.Info, evt.isViewOnce(), false, previouslyConvertedPart, ) + evt.addMuseProfile(portal, cm) if evt.isUndecryptableUpsertSubEvent && isFailedMedia(cm) { evt.postHandle = func() { evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), cm, false) @@ -281,11 +282,30 @@ func (evt *WAMessageEvent) HandleExisting(ctx context.Context, portal *bridgev2. return bridgev2.UpsertResult{}, nil } +func (evt *WAMessageEvent) addMuseProfile(portal *bridgev2.Portal, converted *bridgev2.ConvertedMessage) { + if evt.Info.Chat != types.MuseJID || evt.Info.IsFromMe || !portal.NameIsCustom { + return + } + profile := &event.BeeperPerMessageProfile{ + ID: string(portal.MXID), + Displayname: portal.Name, + } + if portal.AvatarMXC != "" { + profile.AvatarURL = &portal.AvatarMXC + } + for _, part := range converted.Parts { + if part.Content != nil { + part.Content.BeeperPerMessageProfile = profile + } + } +} + func (evt *WAMessageEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) { evt.wa.EnqueuePortalResync(portal, false) converted := evt.wa.Main.MsgConv.ToMatrix( ctx, portal, evt.wa.Client, intent, evt.Message, evt.MsgEvent.RawMessage, &evt.Info, evt.isViewOnce(), false, nil, ) + evt.addMuseProfile(portal, converted) if isFailedMedia(converted) { evt.postHandle = func() { evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), converted, false) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index dba4f1e..f401098 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -334,6 +334,12 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa wa.Main.Bridge.Config.Backfill.Enabled { wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification) } + if protocol := evt.Message.GetProtocolMessage(); evt.Info.Chat == types.MuseJID && protocol.GetType() == waE2E.ProtocolMessage_AI_METADATA_OPERATION { + if sync := protocol.GetAiMetadataOperation().GetHatchMetadataSync(); sync != nil { + return wa.handleMuseMetadata(ctx, sync.GetData()) + } + return + } if parsedMessageType == "ignore" { return } else if strings.HasPrefix(parsedMessageType, "unknown_protocol_") { diff --git a/pkg/connector/muse.go b/pkg/connector/muse.go new file mode 100644 index 0000000..73c4206 --- /dev/null +++ b/pkg/connector/muse.go @@ -0,0 +1,80 @@ +package connector + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/rs/zerolog" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/types" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/networkid" + "maunium.net/go/mautrix/bridgev2/simplevent" +) + +type museImage struct { + DirectPath string `json:"direct_path"` + EncHash []byte `json:"file_enc_sha256_b64"` + Hash []byte `json:"file_sha256_b64"` + Key []byte `json:"media_key_b64"` + Length int `json:"file_length"` +} + +func (wa *WhatsAppClient) handleMuseMetadata(ctx context.Context, data []byte) bool { + var msg struct { + Type string `json:"type"` + Payload struct { + Event string `json:"event"` + Payload struct { + Name string `json:"name"` + Avatar struct { + Image museImage `json:"secure_image"` + } `json:"avatar"` + } `json:"payload"` + } `json:"payload"` + } + if err := json.Unmarshal(data, &msg); err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to decode Muse metadata") + return true + } + if msg.Type != "event" || msg.Payload.Event != "identity.updated" || msg.Payload.Payload.Name == "" { + return true + } + name := msg.Payload.Payload.Name + return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{ + EventMeta: simplevent.EventMeta{ + Type: bridgev2.RemoteEventChatInfoChange, + PortalKey: wa.makeWAPortalKey(types.MuseJID), + CreatePortal: true, + }, + ChatInfoChange: &bridgev2.ChatInfoChange{ + ChatInfo: &bridgev2.ChatInfo{ + Name: &name, + Avatar: wa.museAvatar(msg.Payload.Payload.Avatar.Image), + ExcludeChangesFromTimeline: true, + }, + }, + }).Success +} + +func (wa *WhatsAppClient) museAvatar(image museImage) *bridgev2.Avatar { + if image.DirectPath == "" || image.Length <= 0 || image.Length > 5<<20 { + return nil + } + if len(image.EncHash) != 32 || len(image.Hash) != 32 || len(image.Key) != 32 { + return nil + } + return &bridgev2.Avatar{ + ID: networkid.AvatarID(base64.StdEncoding.EncodeToString(image.Hash)), + Get: func(ctx context.Context) ([]byte, error) { + data, err := wa.Client.DownloadMediaWithPath(ctx, image.DirectPath, image.EncHash, image.Hash, image.Key, whatsmeow.MediaImage, "image", false) + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to download Muse avatar") + return nil, fmt.Errorf("failed to download Muse avatar: %w", err) + } + return data, nil + }, + } +} diff --git a/pkg/msgconv/wa-rich-response.go b/pkg/msgconv/wa-rich-response.go index d2dab51..41ac9ab 100644 --- a/pkg/msgconv/wa-rich-response.go +++ b/pkg/msgconv/wa-rich-response.go @@ -26,6 +26,7 @@ import ( "github.com/rs/zerolog" "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" "go.mau.fi/whatsmeow/types/richresponse" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/event" @@ -42,10 +43,20 @@ func (mc *MessageConverter) convertUnifiedRichResponseMessage(ctx context.Contex } var rrMsg richresponse.RichResponse err := json.Unmarshal(data, &rrMsg) - if err != nil || len(rrMsg.Sections) == 0 { - if err != nil { - zerolog.Ctx(ctx).Err(err).Msg("Failed to unmarshal unified rich response message") + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to unmarshal unified rich response message") + rrMsg.Sections = nil + } + var htmlBuf strings.Builder + for _, mv := range rrMsg.Sections { + if mv.Model == nil { + continue } + for _, p := range mv.Model.GetPrimitives() { + mc.convertRichResponsePrimitive(ctx, p, &htmlBuf) + } + } + if htmlBuf.Len() == 0 { return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, Content: &event.MessageEventContent{ @@ -56,12 +67,6 @@ func (mc *MessageConverter) convertUnifiedRichResponseMessage(ctx context.Contex } } extra["fi.mau.whatsapp.rich_response_id"] = rrMsg.ResponseID - var htmlBuf strings.Builder - for _, mv := range rrMsg.Sections { - for _, p := range mv.Model.GetPrimitives() { - mc.convertRichResponsePrimitive(ctx, p, &htmlBuf) - } - } return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, Content: new(format.HTMLToContent(htmlBuf.String())), @@ -69,7 +74,7 @@ func (mc *MessageConverter) convertUnifiedRichResponseMessage(ctx context.Contex } } -var mdRender = goldmark.New(format.Extensions, format.HTMLOptions, goldmark.WithExtensions(mdext.EscapeHTML)) +var mdRender = goldmark.New(format.HTMLOptions, goldmark.WithExtensions(extension.Strikethrough, extension.Table, mdext.EscapeHTML, mdext.ShortEmphasis, mdext.Math)) func (mc *MessageConverter) convertRichMarkdownText(ctx context.Context, text *richresponse.GenAIMarkdownTextUXPrimitive, buf *strings.Builder) error { for _, ent := range text.InlineEntities { @@ -113,7 +118,7 @@ func (mc *MessageConverter) convertRichResponsePrimitive(ctx context.Context, pr buf.WriteString("
")
 		}
 		for _, part := range p.CodeBlocks {
-			buf.WriteString(part.Content)
+			buf.WriteString(html.EscapeString(part.Content))
 		}
 		buf.WriteString("
") case *richresponse.GenAIMarkdownTextUXPrimitive: @@ -122,7 +127,7 @@ func (mc *MessageConverter) convertRichResponsePrimitive(ctx context.Context, pr buf.WriteString("") for _, row := range p.Rows { buf.WriteString("") - cellType := "tr" + cellType := "td" if row.IsHeader { cellType = "th" } From 4fc155c597a2407e015a5279c50de6f57c668cc9 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 28 Sep 2026 17:05:47 +0300 Subject: [PATCH 067/139] dependencies: update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index dcae3bb..c5b87b2 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/yuin/goldmark v1.8.6 go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260927171547-45cfce066cd2 + go.mau.fi/whatsmeow v0.0.0-20260928140511-35f522c88ce3 golang.org/x/image v0.46.0 golang.org/x/net v0.59.0 golang.org/x/sync v0.23.0 diff --git a/go.sum b/go.sum index 82983af..6b66cf9 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcU go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= 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-20260927171547-45cfce066cd2 h1:MdSiBaMjvUIFbk8Cqf90CJU0JrRYXnvGKQNoM3ciaas= -go.mau.fi/whatsmeow v0.0.0-20260927171547-45cfce066cd2/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= +go.mau.fi/whatsmeow v0.0.0-20260928140511-35f522c88ce3 h1:ix7+8nGKloIdLhGeggvKllb5N/J6hfwImBy/xzOvrgY= +go.mau.fi/whatsmeow v0.0.0-20260928140511-35f522c88ce3/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= 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.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= From 2ffea1248ab9ddcdb83712d2a7e85d277d51925e Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 29 Sep 2026 19:15:59 +0000 Subject: [PATCH 068/139] client: automatically request muse key and profile if missing (#960) --- go.mod | 6 +- go.sum | 8 +-- pkg/connector/chatinfo.go | 3 + pkg/connector/client.go | 8 +++ pkg/connector/handlewhatsapp.go | 6 ++ pkg/connector/login.go | 1 + pkg/connector/muse.go | 106 +++++++++++++++++++++++++++++++- pkg/waid/dbmeta.go | 1 + 8 files changed, 131 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index c5b87b2..38c4495 100644 --- a/go.mod +++ b/go.mod @@ -7,19 +7,20 @@ toolchain go1.27.1 tool go.mau.fi/util/cmd/maubuild require ( + github.com/google/uuid v1.6.0 github.com/lib/pq v1.12.3 github.com/rs/zerolog v1.35.1 github.com/tidwall/gjson v1.19.0 github.com/yuin/goldmark v1.8.6 go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260928140511-35f522c88ce3 + go.mau.fi/whatsmeow v0.0.0-20260929112325-8b41cfe6d9c4 golang.org/x/image v0.46.0 golang.org/x/net v0.59.0 golang.org/x/sync v0.23.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.31.1-0.20260928113010-0d8772dfa579 + maunium.net/go/mautrix v0.31.1-0.20260929190113-4aac2bbf2888 ) require ( @@ -28,7 +29,6 @@ require ( github.com/coder/websocket v1.8.15 // indirect github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index 6b66cf9..954ffc1 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcU go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= 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-20260928140511-35f522c88ce3 h1:ix7+8nGKloIdLhGeggvKllb5N/J6hfwImBy/xzOvrgY= -go.mau.fi/whatsmeow v0.0.0-20260928140511-35f522c88ce3/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= +go.mau.fi/whatsmeow v0.0.0-20260929112325-8b41cfe6d9c4 h1:0PXBB/t9JjRLCyv8Dp5K1OyE+ut/6VgKZqGUsaAaPNk= +go.mau.fi/whatsmeow v0.0.0-20260929112325-8b41cfe6d9c4/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= 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.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= @@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.31.1-0.20260928113010-0d8772dfa579 h1:CfIu/q0W0z8uLR9o6C0kcFQqFTTWUi2j9+Vd9yQV1xg= -maunium.net/go/mautrix v0.31.1-0.20260928113010-0d8772dfa579/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk= +maunium.net/go/mautrix v0.31.1-0.20260929190113-4aac2bbf2888 h1:sC0fpamjHE5Q2nqy9frxv30rHcX53xPg0FRZ3C2IVoU= +maunium.net/go/mautrix v0.31.1-0.20260929190113-4aac2bbf2888/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk= diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 267200c..7635e86 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -35,6 +35,9 @@ func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, switch portalJID.Server { case types.DefaultUserServer, types.HiddenUserServer, types.BotServer: wrapped = wa.wrapDMInfo(ctx, portalJID) + if portalJID == types.MuseJID { + go wa.requestMuseProfile(ctx) + } case types.BroadcastServer: if portalJID == types.StatusBroadcastJID { wrapped = wa.wrapStatusBroadcastInfo(ctx) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 2287fea..9a54fcd 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -127,6 +127,10 @@ type WhatsAppClient struct { appStateRecoveryLock sync.Mutex appStateFullSyncAttempted map[appstate.WAPatchName]time.Time + + wasaResyncLock sync.Mutex + museProfileLock sync.Mutex + lastMuseProfileRequest time.Time } var ( @@ -460,6 +464,10 @@ func (wa *WhatsAppClient) HandleMatrixViewingChat(ctx context.Context, msg *brid } } + if msg.Portal != nil && msg.Portal.ID == waid.MakePortalID(types.MuseJID) { + go wa.requestMuseProfile(ctx) + } + if msg.Portal == nil || msg.Portal.Metadata.(*waid.PortalMetadata).LastSync.Add(5*time.Minute).After(time.Now()) { // If we resynced this portal within the last 5 minutes, don't do it again return nil diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index f401098..d320ad1 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -174,6 +174,12 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { case *events.Connected: log.Debug().Msg("Connected to WhatsApp socket") wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) + if wa.offlineSyncWaiter.Load() == nil { + go func() { + wa.resyncWASARootSecrets(ctx) + wa.requestMuseProfile(ctx) + }() + } if len(wa.GetStore().PushName) > 0 { go func() { err := wa.updatePresence(ctx, types.PresenceUnavailable) diff --git a/pkg/connector/login.go b/pkg/connector/login.go index fa2c479..e5f7cd4 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -402,6 +402,7 @@ func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, er Timezone: wl.Timezone, HistorySyncPortalsNeedCreating: true, + WASAResynced: true, }, }, &bridgev2.NewLoginParams{ DeleteOnConflict: true, diff --git a/pkg/connector/muse.go b/pkg/connector/muse.go index 73c4206..94a25d9 100644 --- a/pkg/connector/muse.go +++ b/pkg/connector/muse.go @@ -5,13 +5,24 @@ import ( "encoding/base64" "encoding/json" "fmt" + "runtime/debug" + "time" + "github.com/google/uuid" "github.com/rs/zerolog" + "go.mau.fi/util/exerrors" "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/appstate" + "go.mau.fi/whatsmeow/proto/waAICommon" + "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/types" + "google.golang.org/protobuf/proto" "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/simplevent" + + "go.mau.fi/mautrix-whatsapp/pkg/waid" ) type museImage struct { @@ -22,6 +33,93 @@ type museImage struct { Length int `json:"file_length"` } +func (wa *WhatsAppClient) resyncWASARootSecrets(ctx context.Context) { + defer func() { + v := recover() + if v != nil { + zerolog.Ctx(ctx).Err(exerrors.RecoverToError(v)). + Bytes("stack", debug.Stack()). + Msg("Error resyncing WASA root secrets") + } + }() + wa.wasaResyncLock.Lock() + defer wa.wasaResyncLock.Unlock() + if wa.offlineSyncWaiter.Load() != nil || !wa.Client.IsConnected() { + return + } + meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata) + if meta.WASAResynced { + return + } + log := zerolog.Ctx(ctx) + log.Info().Msg("Resyncing WASA root secrets for existing login") + if err := wa.Client.FetchAppState(ctx, appstate.WAPatchRegularHigh, true, false); err != nil { + log.Err(err).Msg("Failed to resync WASA root secrets") + return + } + meta.WASAResynced = true + if err := wa.UserLogin.Save(ctx); err != nil { + meta.WASAResynced = false + log.Err(err).Msg("Failed to save WASA resync completion") + return + } + log.Info().Msg("Completed WASA root secret resync") +} + +func (wa *WhatsAppClient) requestMuseProfile(ctx context.Context) { + defer func() { + v := recover() + if v != nil { + zerolog.Ctx(ctx).Err(exerrors.RecoverToError(v)). + Bytes("stack", debug.Stack()). + Msg("Error requesting Muse profile") + } + }() + if !wa.museProfileLock.TryLock() { + return + } + defer wa.museProfileLock.Unlock() + if wa.offlineSyncWaiter.Load() != nil || !wa.Client.IsConnected() || time.Since(wa.lastMuseProfileRequest) < 5*time.Minute { + return + } + portal, err := wa.Main.Bridge.DB.Portal.GetByKey(ctx, wa.makeWAPortalKey(types.MuseJID)) + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to check Muse profile") + return + } else if portal != nil && portal.NameIsCustom && portal.Name != "" && portal.NameSet && museAvatarReady(portal) { + return + } + rootID, err := wa.GetStore().ChatSettings.GetWASARootSecretID(ctx, types.MuseJID) + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to check Muse root secret") + return + } else if rootID == "" { + return + } + wa.lastMuseProfileRequest = time.Now() + _, err = wa.Client.SendMessage(ctx, types.MuseJID, &waE2E.Message{ + ProtocolMessage: &waE2E.ProtocolMessage{ + Type: waE2E.ProtocolMessage_AI_METADATA_OPERATION.Enum(), + AiMetadataOperation: &waAICommon.AIMetadataOperation{ + HatchMetadataSync: &waAICommon.HatchMetadataSync{ + Data: []byte(`{"version":1,"type":"req","payload":{"method":"channel.bootstrap","params":{"sections":["agent.status","identity.updated","hitl.snapshot"]}}}`), + TimestampMS: proto.Int64(time.Now().UnixMilli()), + RequestID: proto.String(uuid.NewString()), + }, + }, + }, + }) + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to request Muse profile") + } +} + +func museAvatarReady(portal *database.Portal) bool { + expectedHash, err := base64.StdEncoding.DecodeString(string(portal.AvatarID)) + return err != nil || len(expectedHash) != 32 || + (portal.AvatarMXC != "" && portal.AvatarHash == [32]byte(expectedHash)) +} + func (wa *WhatsAppClient) handleMuseMetadata(ctx context.Context, data []byte) bool { var msg struct { Type string `json:"type"` @@ -43,16 +141,22 @@ func (wa *WhatsAppClient) handleMuseMetadata(ctx context.Context, data []byte) b return true } name := msg.Payload.Payload.Name + avatar := wa.museAvatar(msg.Payload.Payload.Avatar.Image) return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventChatInfoChange, PortalKey: wa.makeWAPortalKey(types.MuseJID), CreatePortal: true, + PreHandleFunc: func(_ context.Context, portal *bridgev2.Portal) { + if avatar != nil && !museAvatarReady(portal.Portal) { + portal.AvatarSet = false + } + }, }, ChatInfoChange: &bridgev2.ChatInfoChange{ ChatInfo: &bridgev2.ChatInfo{ Name: &name, - Avatar: wa.museAvatar(msg.Payload.Payload.Avatar.Image), + Avatar: avatar, ExcludeChangesFromTimeline: true, }, }, diff --git a/pkg/waid/dbmeta.go b/pkg/waid/dbmeta.go index e2b60f7..6e018f6 100644 --- a/pkg/waid/dbmeta.go +++ b/pkg/waid/dbmeta.go @@ -40,6 +40,7 @@ type UserLoginMetadata struct { LoggedInAt jsontime.Unix `json:"logged_in_at,omitzero"` AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"` + WASAResynced bool `json:"wasa_resynced,omitzero"` HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitzero"` ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitzero"` From 9f436b26dbb27f93cf1351a403d056431edbe061 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 29 Sep 2026 22:17:42 +0300 Subject: [PATCH 069/139] muse: log when doing initial profile sync --- pkg/connector/muse.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/connector/muse.go b/pkg/connector/muse.go index 94a25d9..7c62144 100644 --- a/pkg/connector/muse.go +++ b/pkg/connector/muse.go @@ -79,7 +79,7 @@ func (wa *WhatsAppClient) requestMuseProfile(ctx context.Context) { return } defer wa.museProfileLock.Unlock() - if wa.offlineSyncWaiter.Load() != nil || !wa.Client.IsConnected() || time.Since(wa.lastMuseProfileRequest) < 5*time.Minute { + if wa.offlineSyncWaiter.Load() != nil || !wa.Client.IsConnected() || time.Since(wa.lastMuseProfileRequest) < 15*time.Minute { return } portal, err := wa.Main.Bridge.DB.Portal.GetByKey(ctx, wa.makeWAPortalKey(types.MuseJID)) @@ -96,6 +96,7 @@ func (wa *WhatsAppClient) requestMuseProfile(ctx context.Context) { } else if rootID == "" { return } + zerolog.Ctx(ctx).Info().Msg("Sending request to sync Muse profile") wa.lastMuseProfileRequest = time.Now() _, err = wa.Client.SendMessage(ctx, types.MuseJID, &waE2E.Message{ ProtocolMessage: &waE2E.ProtocolMessage{ From e1f141f4478a462e8de1b082e73d88e017f87bbc Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 30 Sep 2026 19:00:20 +0300 Subject: [PATCH 070/139] handlewhatsapp: log inner message type in edits --- pkg/connector/events.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/connector/events.go b/pkg/connector/events.go index c5cdac8..bd9f9bf 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -136,6 +136,9 @@ func (evt *WAMessageEvent) AddLogContext(c zerolog.Context) zerolog.Context { if targetMsg := evt.GetTargetMessage(); targetMsg != "" { c = c.Str("target_message_id", string(targetMsg)) } + if evt.parsedMessageType == "edit" { + c = c.Str("edit_inner_message_type", getMessageType(evt.Message.GetProtocolMessage().GetEditedMessage())) + } return evt.MessageInfoWrapper.AddLogContext(c).Str("parsed_message_type", evt.parsedMessageType) } From cafeca69796379abf80df1df9f6b6fdf97c21c25 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 30 Sep 2026 19:20:36 +0300 Subject: [PATCH 071/139] handlewhatsapp: unwrap some message types in edits --- pkg/connector/events.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/connector/events.go b/pkg/connector/events.go index bd9f9bf..bc11307 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -192,12 +192,18 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por cacheMessage = evt.GetID() } else { editedMsg = evt.Message.GetProtocolMessage().GetEditedMessage() - previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, targetMessage) + if editedMsg.GetLottieStickerMessage().GetMessage() != nil { + editedMsg = editedMsg.GetLottieStickerMessage().GetMessage() + } + if editedMsg.GetDocumentWithCaptionMessage().GetMessage() != nil { + editedMsg = editedMsg.GetDocumentWithCaptionMessage().GetMessage() + } meta := existing[0].Metadata.(*waid.MessageMetadata) if slices.Contains(meta.Edits, evt.Info.ID) { return nil, fmt.Errorf("%w: edit already handled", bridgev2.ErrIgnoringRemoteEvent) } meta.Edits = append(meta.Edits, evt.Info.ID) + previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, targetMessage) } ctx = context.WithValue(ctx, msgconv.ContextKeyEditTargetID, evt.Message.GetProtocolMessage().GetKey().GetID()) From c84643e70ad92a62e4e7f7096c5a56ec7fdfa5f8 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 30 Sep 2026 19:20:58 +0300 Subject: [PATCH 072/139] handlewhatsapp: try to fetch message from server for caption edit --- pkg/connector/events.go | 24 ++++++++++++++++++++++++ pkg/connector/wamsgtype.go | 10 ++++++++++ 2 files changed, 34 insertions(+) diff --git a/pkg/connector/events.go b/pkg/connector/events.go index bc11307..bd328c5 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -204,6 +204,30 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por } meta.Edits = append(meta.Edits, evt.Info.ID) previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, targetMessage) + if previouslyConvertedPart == nil && needsPreviousEditPart(editedMsg) { + editedEvent, err := evt.wa.Main.Bridge.Bot.GetEvent(ctx, portal.MXID, existing[0].MXID) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to fetch existing event for edit") + } else if editedEvent != nil && editedEvent.Content.AsMessage().MsgType != "" { + zerolog.Ctx(ctx).Debug().Msg("Fetched existing edit from server for caption edit") + copiedExtra := make(map[string]any) + for _, key := range []string{"info", msgconv.FailedMediaField} { + val, ok := editedEvent.Content.Raw[key] + if ok { + copiedExtra[key] = val + } + } + previouslyConvertedPart = &bridgev2.ConvertedMessagePart{ + ID: existing[0].PartID, + Type: editedEvent.Type, + Content: editedEvent.Content.AsMessage(), + Extra: copiedExtra, + DBMetadata: meta, + } + } else { + zerolog.Ctx(ctx).Debug().Msg("Didn't find existing edit on server for caption edit") + } + } } ctx = context.WithValue(ctx, msgconv.ContextKeyEditTargetID, evt.Message.GetProtocolMessage().GetKey().GetID()) diff --git a/pkg/connector/wamsgtype.go b/pkg/connector/wamsgtype.go index 0a29b82..efdf679 100644 --- a/pkg/connector/wamsgtype.go +++ b/pkg/connector/wamsgtype.go @@ -159,3 +159,13 @@ func getMessageType(waMsg *waE2E.Message) string { return "unknown" } } + +func needsPreviousEditPart(waMsg *waE2E.Message) bool { + switch { + case waMsg.ImageMessage != nil, waMsg.StickerMessage != nil, waMsg.VideoMessage != nil, + waMsg.PtvMessage != nil, waMsg.AudioMessage != nil, waMsg.DocumentMessage != nil: + return true + default: + return false + } +} From a0325e76df3341136ab344dd3bd8401710117e42 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 30 Sep 2026 19:34:14 +0300 Subject: [PATCH 073/139] msgconv/from-whatsapp: don't create direct media url for empty paths --- pkg/msgconv/wa-media.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go index 9964800..20a1a14 100644 --- a/pkg/msgconv/wa-media.go +++ b/pkg/msgconv/wa-media.go @@ -100,13 +100,17 @@ func (mc *MessageConverter) convertMediaMessage( if err != nil { panic(err) } - part = &bridgev2.ConvertedMessagePart{ - Type: preparedMedia.Type, - Content: preparedMedia.MessageEventContent, - Extra: preparedMedia.Extra, - DBMetadata: &waid.MessageMetadata{ - DirectMediaMeta: directMediaMeta, - }, + if mediaKeys.DirectPath == "" { + part = mc.makeMediaFailure(ctx, preparedMedia, mediaKeys, whatsmeow.ErrNoURLPresent) + } else { + part = &bridgev2.ConvertedMessagePart{ + Type: preparedMedia.Type, + Content: preparedMedia.MessageEventContent, + Extra: preparedMedia.Extra, + DBMetadata: &waid.MessageMetadata{ + DirectMediaMeta: directMediaMeta, + }, + } } } else if err := mc.reuploadWhatsAppAttachment(ctx, msg, preparedMedia); err != nil { part = mc.makeMediaFailure(ctx, preparedMedia, mediaKeys, err) From d97b19124562944386bc1776fafaef98ac603b91 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Tue, 6 Oct 2026 10:38:35 +0100 Subject: [PATCH 074/139] backfill: prevent panic when converting history sync messages (#955) --- pkg/connector/backfill.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 50a0d4c..85f4499 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -628,7 +628,8 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( messages []*waWeb.WebMessageInfo, explodeOnError bool, ) (*bridgev2.FetchMessagesResponse, error) { - if wa.Client == nil { + cli := wa.Client + if cli == nil { return nil, bridgev2.ErrNotLoggedIn } oldestTS := messages[len(messages)-1].GetMessageTimestamp() @@ -637,7 +638,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( var mediaRequests []*wadb.MediaRequest dups := make(exmaps.Set[networkid.MessageID]) for i, msg := range messages { - evt, err := wa.Client.ParseWebMessage(portalJID, msg) + evt, err := cli.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 @@ -664,7 +665,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( } isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension converted, mediaReq := wa.convertHistorySyncMessage( - ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, + ctx, cli, 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 @@ -805,7 +806,7 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w } func (wa *WhatsAppClient) convertHistorySyncMessage( - ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction, + ctx context.Context, cli *whatsmeow.Client, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction, ) (*bridgev2.BackfillMessage, *wadb.MediaRequest) { // New messages turn these into edits, but in backfill we only have the last version, // so no need to do the edit thing. Instead, just unwrap the message. @@ -816,7 +817,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessage( intent := wa.Main.Bridge.Bot msgID := waid.MakeMessageIDWithAltSender(info.Chat, info.Sender, info.SenderAlt, info.ID) wrapped := &bridgev2.BackfillMessage{ - ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil), + ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, cli, intent, msg, rawMsg, info, isViewOnce, true, nil), Sender: wa.makeEventSender(ctx, pickLID(info.Sender, info.SenderAlt)), ID: msgID, TxnID: networkid.TransactionID(msgID), From 06b42e140cd48284b3220c4e1b5796a6b9d1ae69 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 6 Oct 2026 12:52:01 +0300 Subject: [PATCH 075/139] muse: mark resync as done on mismatching lthash --- pkg/connector/muse.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/connector/muse.go b/pkg/connector/muse.go index 7c62144..ee6b04d 100644 --- a/pkg/connector/muse.go +++ b/pkg/connector/muse.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "runtime/debug" "time" @@ -53,9 +54,15 @@ func (wa *WhatsAppClient) resyncWASARootSecrets(ctx context.Context) { } log := zerolog.Ctx(ctx) log.Info().Msg("Resyncing WASA root secrets for existing login") + success := true if err := wa.Client.FetchAppState(ctx, appstate.WAPatchRegularHigh, true, false); err != nil { log.Err(err).Msg("Failed to resync WASA root secrets") - return + // Mark the sync as completed on mismatching LTHash errors, because those can get permanently stuck + // (retrying won't help). In case it's not stuck, a single call should be enough to request a resync. + if !errors.Is(err, appstate.ErrMismatchingLTHash) || ctx.Err() != nil { + return + } + success = false } meta.WASAResynced = true if err := wa.UserLogin.Save(ctx); err != nil { @@ -63,7 +70,11 @@ func (wa *WhatsAppClient) resyncWASARootSecrets(ctx context.Context) { log.Err(err).Msg("Failed to save WASA resync completion") return } - log.Info().Msg("Completed WASA root secret resync") + if success { + log.Info().Msg("Completed WASA root secret resync") + } else { + log.Info().Msg("Marked WASA root resync as done despite errors") + } } func (wa *WhatsAppClient) requestMuseProfile(ctx context.Context) { From a34259020e60cf6e16cf5002e5d885ee03a3dd15 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 6 Oct 2026 15:22:21 +0300 Subject: [PATCH 076/139] handle*: add support for blocking DMs --- go.mod | 4 +- go.sum | 8 +-- pkg/connector/capabilities.go | 15 +++++- pkg/connector/chatinfo.go | 21 ++++++++ pkg/connector/client.go | 1 + pkg/connector/handlematrix.go | 48 +++++++++++++++++ pkg/connector/handlewhatsapp.go | 92 +++++++++++++++++++++++++++++++++ pkg/waid/dbmeta.go | 2 + 8 files changed, 183 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 38c4495..b6398d4 100644 --- a/go.mod +++ b/go.mod @@ -14,13 +14,13 @@ require ( github.com/yuin/goldmark v1.8.6 go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260929112325-8b41cfe6d9c4 + go.mau.fi/whatsmeow v0.0.0-20261006124319-9399289b022b golang.org/x/image v0.46.0 golang.org/x/net v0.59.0 golang.org/x/sync v0.23.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.31.1-0.20260929190113-4aac2bbf2888 + maunium.net/go/mautrix v0.31.1-0.20261005174338-57e1ea5dd74d ) require ( diff --git a/go.sum b/go.sum index 954ffc1..5854082 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcU go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= 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-20260929112325-8b41cfe6d9c4 h1:0PXBB/t9JjRLCyv8Dp5K1OyE+ut/6VgKZqGUsaAaPNk= -go.mau.fi/whatsmeow v0.0.0-20260929112325-8b41cfe6d9c4/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= +go.mau.fi/whatsmeow v0.0.0-20261006124319-9399289b022b h1:sxHC8T2zT69QJmFx6zuB3KuDu+49KFf28bszKtv8vXg= +go.mau.fi/whatsmeow v0.0.0-20261006124319-9399289b022b/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= 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.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= @@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.31.1-0.20260929190113-4aac2bbf2888 h1:sC0fpamjHE5Q2nqy9frxv30rHcX53xPg0FRZ3C2IVoU= -maunium.net/go/mautrix v0.31.1-0.20260929190113-4aac2bbf2888/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk= +maunium.net/go/mautrix v0.31.1-0.20261005174338-57e1ea5dd74d h1:sOdF5AF9MiU2mudfMBgQC79FJrhBBDbUkxQCi0T2uHI= +maunium.net/go/mautrix v0.31.1-0.20261005174338-57e1ea5dd74d/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk= diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index 152e7ad..b991893 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -52,7 +52,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit } func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) { - return 1, 9 + return 1, 10 } const WAMaxFileSize = 2000 * 1024 * 1024 @@ -67,7 +67,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel { } func capID() string { - base := "fi.mau.whatsapp.capabilities.2026_07_22" + base := "fi.mau.whatsapp.capabilities.2026_10_01" if ffmpeg.Supported() { return base + "+ffmpeg" } @@ -198,6 +198,7 @@ var whatsappCaps = &event.RoomFeatures{ var whatsappDMCaps *event.RoomFeatures var whatsappCAGCaps *event.RoomFeatures +var whatsappNoteToSelfCaps *event.RoomFeatures func init() { whatsappDMCaps = ptr.Clone(whatsappCaps) @@ -205,7 +206,13 @@ func init() { whatsappDMCaps.State = event.StateFeatureMap{ event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported}, } + whatsappDMCaps.BlockUser = true whatsappDMCaps.MemberActions = nil + + whatsappNoteToSelfCaps = ptr.Clone(whatsappDMCaps) + whatsappNoteToSelfCaps.ID = capID() + "+self" + whatsappNoteToSelfCaps.BlockUser = false + whatsappCAGCaps = ptr.Clone(whatsappCaps) whatsappCAGCaps.ID = capID() + "+cag" whatsappCAGCaps.Reply = event.CapLevelUnsupported @@ -216,6 +223,10 @@ func (wa *WhatsAppClient) GetCapabilities(ctx context.Context, portal *bridgev2. if portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup { return whatsappCAGCaps } else if portal.RoomType == database.RoomTypeDM { + jid, err := waid.ParsePortalID(portal.ID) + if err == nil && wa.IsOwnJID(jid) { + return whatsappNoteToSelfCaps + } return whatsappDMCaps } return whatsappCaps diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 7635e86..3984b5f 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -185,6 +185,11 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge }, ExcludeChangesFromTimeline: true, } + var err error + info.UserBlocked, err = wa.isUserBlocked(ctx, jid) + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get blocked state") + } if jid.Server == types.BotServer { info.Topic = ptr.Ptr(BotChatTopic) } @@ -571,3 +576,19 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne Type: ptr.Ptr(database.RoomTypeDefault), } } + +func (wa *WhatsAppClient) isUserBlocked(ctx context.Context, jid types.JID) (*bool, error) { + wa.blocklistLock.Lock() + defer wa.blocklistLock.Unlock() + meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata) + blocklist := meta.Blocklist + if blocklist == nil { + runHandlers := wa.resyncBlocklistUnlocked(ctx) + go runHandlers() + blocklist = meta.Blocklist + } + if blocklist == nil { + return nil, nil + } + return new(blocklist.IsBlocked(jid)), nil +} diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 9a54fcd..61fceec 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -122,6 +122,7 @@ type WhatsAppClient struct { isNewLogin bool pushNamesSynced *exsync.Event lastPresence types.Presence + blocklistLock sync.Mutex disableNewsletter bool diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index c55f523..a646ac9 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -1,3 +1,19 @@ +// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2026 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + package connector import ( @@ -19,6 +35,7 @@ import ( "go.mau.fi/whatsmeow/proto/waCommon" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/types" + "go.mau.fi/whatsmeow/types/events" "golang.org/x/image/draw" "google.golang.org/protobuf/proto" "maunium.net/go/mautrix/bridgev2" @@ -46,6 +63,7 @@ var ( _ bridgev2.TagHandlingNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.MarkedUnreadHandlingNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.DeleteChatHandlingNetworkAPI = (*WhatsAppClient)(nil) + _ bridgev2.UserBlockingNetworkAPI = (*WhatsAppClient)(nil) ) func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (result *bridgev2.MatrixMessageResponse, retErr error) { @@ -691,3 +709,33 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg defer wa.mcTrack(msg, time.Now(), &retErr) return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true)) } + +func (wa *WhatsAppClient) HandleMatrixBlockUser(ctx context.Context, msg *bridgev2.MatrixBlockUser) error { + if wa.Client == nil { + return bridgev2.ErrNotLoggedIn + } + jid, err := waid.ParsePortalID(msg.Portal.ID) + if err != nil { + return err + } + if msg.Content.ReportSpam { + return fmt.Errorf("spam reporting is not supported") + } + action := events.BlocklistChangeActionUnblock + if msg.Content.Block { + action = events.BlocklistChangeActionBlock + } + blocklist, err := wa.Client.UpdateBlocklist(ctx, jid, action, "") + if err == nil { + wa.blocklistLock.Lock() + evts := wa.saveBlocklistUnlocked(ctx, blocklist) + wa.blocklistLock.Unlock() + for _, evt := range evts { + if evt.PortalKey == msg.Portal.PortalKey { + continue + } + wa.UserLogin.QueueRemoteEvent(evt) + } + } + return err +} diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index d320ad1..117cfad 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -82,6 +82,8 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { success = true switch evt := rawEvt.(type) { + case *events.Blocklist: + success = wa.resyncBlocklist(ctx)() case *events.Message: success = wa.handleWAMessage(ctx, evt) case *events.Receipt: @@ -1016,3 +1018,93 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev } }() } + +func (wa *WhatsAppClient) resyncBlocklist(ctx context.Context) func() bool { + wa.blocklistLock.Lock() + defer wa.blocklistLock.Unlock() + return wa.resyncBlocklistUnlocked(ctx) +} + +func noopUpdates() bool { + return true +} + +func (wa *WhatsAppClient) resyncBlocklistUnlocked(ctx context.Context) func() bool { + meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata) + dhash := ptr.Val(meta.Blocklist).DHash + newBlocklist, err := wa.Client.GetBlocklist(ctx, dhash) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to get new blocklist") + return noopUpdates + } else if newBlocklist == nil { + zerolog.Ctx(ctx).Debug().Msg("Blocklist didn't change") + return noopUpdates + } + updateEvents := wa.saveBlocklistUnlocked(ctx, newBlocklist) + return func() bool { + for _, evt := range updateEvents { + res := wa.UserLogin.QueueRemoteEvent(evt) + if !res.Success { + return false + } + } + return true + } +} + +func (wa *WhatsAppClient) saveBlocklistUnlocked(ctx context.Context, newBlocklist *types.Blocklist) []*simplevent.ChatInfoChange { + if newBlocklist.AddressingMode != types.AddressingModeLID { + zerolog.Ctx(ctx).Warn().Msg("Non-LID addressing mode in blocklist") + } + meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata) + oldBlocklist := ptr.Val(meta.Blocklist) + removed, added := exslices.Diff(oldBlocklist.LIDs(), newBlocklist.LIDs()) + var updateEvents []*simplevent.ChatInfoChange + for _, jid := range removed { + if evt := wa.makeBlockUpdateEvent(ctx, jid, false); evt != nil { + updateEvents = append(updateEvents, evt) + } + } + for _, jid := range added { + if evt := wa.makeBlockUpdateEvent(ctx, jid, true); evt != nil { + updateEvents = append(updateEvents, evt) + } + } + meta.Blocklist = newBlocklist + err := wa.UserLogin.Save(ctx) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to save blocklist to login metadata") + } + return updateEvents +} + +func (wa *WhatsAppClient) makeBlockUpdateEvent(ctx context.Context, jid types.JID, blocked bool) *simplevent.ChatInfoChange { + if jid.Server == types.DefaultUserServer { + var err error + jid, err = wa.GetStore().LIDs.GetLIDForPN(ctx, jid) + if err != nil { + zerolog.Ctx(ctx).Err(err).Stringer("pn", jid).Msg("Failed to get LID for blocklist update") + return nil + } else if jid.IsEmpty() { + zerolog.Ctx(ctx).Debug().Stringer("pn", jid).Msg("No LID for blocklist update") + return nil + } + } else if jid.Server != types.HiddenUserServer && jid.Server != types.BotServer { + zerolog.Ctx(ctx).Debug().Stringer("jid", jid).Msg("Unexpected JID server in blocklist") + return nil + } + return &simplevent.ChatInfoChange{ + EventMeta: simplevent.EventMeta{ + Type: bridgev2.RemoteEventChatInfoChange, + PortalKey: wa.makeWAPortalKey(jid), + LogContext: func(c zerolog.Context) zerolog.Context { + return c.Str("wa_event_type", "blocklist update") + }, + }, + ChatInfoChange: &bridgev2.ChatInfoChange{ + ChatInfo: &bridgev2.ChatInfo{ + UserBlocked: &blocked, + }, + }, + } +} diff --git a/pkg/waid/dbmeta.go b/pkg/waid/dbmeta.go index 6e018f6..15c0a42 100644 --- a/pkg/waid/dbmeta.go +++ b/pkg/waid/dbmeta.go @@ -45,6 +45,8 @@ type UserLoginMetadata struct { HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitzero"` ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitzero"` + Blocklist *types.Blocklist `json:"blocklist,omitempty"` + MData json.RawMessage `json:"mdata,omitempty"` } From cb7d14e9794300944722ac22c58c200692321e42 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 6 Oct 2026 16:01:26 +0300 Subject: [PATCH 077/139] handlewhatsapp: fetch blocked DMs from db for dispatching updates --- pkg/connector/handlewhatsapp.go | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 117cfad..2b30fdd 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -1058,7 +1058,24 @@ func (wa *WhatsAppClient) saveBlocklistUnlocked(ctx context.Context, newBlocklis } meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata) oldBlocklist := ptr.Val(meta.Blocklist) - removed, added := exslices.Diff(oldBlocklist.LIDs(), newBlocklist.LIDs()) + currentlyBlockedDMs, err := wa.Main.Bridge.DB.Portal.GetAllBlockedDMsOf(ctx, wa.UserLogin.ID) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to get currently blocked DMs") + } + // In addition to diffing the cached blocklist, fetch all blocked DM portals from the database + // to ensure any missed updates are retried. + oldBlockedLIDs := exslices.CastFuncFilter[types.JID](currentlyBlockedDMs, func(key networkid.PortalKey) (types.JID, bool) { + jid, _ := waid.ParsePortalID(key.ID) + return jid, !jid.IsEmpty() + }) + removed, added := exslices.Diff(append(oldBlocklist.LIDs(), oldBlockedLIDs...), newBlocklist.LIDs()) + zerolog.Ctx(ctx).Debug(). + Int("old_count", len(oldBlocklist.Items)). + Int("db_count", len(currentlyBlockedDMs)). + Int("new_count", len(newBlocklist.Items)). + Any("removed", removed). + Any("added", added). + Msg("Syncing blocklist") var updateEvents []*simplevent.ChatInfoChange for _, jid := range removed { if evt := wa.makeBlockUpdateEvent(ctx, jid, false); evt != nil { @@ -1071,7 +1088,7 @@ func (wa *WhatsAppClient) saveBlocklistUnlocked(ctx context.Context, newBlocklis } } meta.Blocklist = newBlocklist - err := wa.UserLogin.Save(ctx) + err = wa.UserLogin.Save(ctx) if err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to save blocklist to login metadata") } From 9959878fe0c3071bb6729d54befb0b843c4b71d3 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 6 Oct 2026 16:05:15 +0300 Subject: [PATCH 078/139] handlematrix: dispatch extra block updates in the background --- pkg/connector/handlematrix.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index a646ac9..ee3d70a 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -728,14 +728,16 @@ func (wa *WhatsAppClient) HandleMatrixBlockUser(ctx context.Context, msg *bridge blocklist, err := wa.Client.UpdateBlocklist(ctx, jid, action, "") if err == nil { wa.blocklistLock.Lock() + defer wa.blocklistLock.Unlock() evts := wa.saveBlocklistUnlocked(ctx, blocklist) - wa.blocklistLock.Unlock() - for _, evt := range evts { - if evt.PortalKey == msg.Portal.PortalKey { - continue + go func() { + for _, evt := range evts { + if evt.PortalKey == msg.Portal.PortalKey { + continue + } + wa.UserLogin.QueueRemoteEvent(evt) } - wa.UserLogin.QueueRemoteEvent(evt) - } + }() } return err } From 5f904a6203db8f1fc45d799514faf5298bb34656 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 6 Oct 2026 13:52:22 +0000 Subject: [PATCH 079/139] msgconv/from-matrix: try to fill outgoing reply quotes (#963) --- pkg/msgconv/from-matrix.go | 36 ++++++++++++++++++++++++++++++++++-- pkg/msgconv/matrixpoll.go | 2 +- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index b3ddcf9..6f1d515 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -53,6 +53,7 @@ import ( func (mc *MessageConverter) generateContextInfo( ctx context.Context, replyTo *database.Message, + relatesTo *event.RelatesTo, portal *bridgev2.Portal, perMessageTimer *event.BeeperDisappearingTimer, roomMention bool, @@ -63,7 +64,7 @@ func (mc *MessageConverter) generateContextInfo( if err == nil { contextInfo.StanzaID = proto.String(msgID.ID) contextInfo.Participant = proto.String(msgID.Sender.String()) - contextInfo.QuotedMessage = &waE2E.Message{Conversation: proto.String("")} + contextInfo.QuotedMessage = mc.getQuotedMessage(ctx, replyTo, relatesTo, portal) contextInfo.QuotedType = waE2E.ContextInfo_EXPLICIT.Enum() } else { zerolog.Ctx(ctx).Warn().Err(err). @@ -91,6 +92,37 @@ func (mc *MessageConverter) generateContextInfo( return contextInfo } +func (mc *MessageConverter) getQuotedMessage(ctx context.Context, replyTo *database.Message, relatesTo *event.RelatesTo, portal *bridgev2.Portal) *waE2E.Message { + log := zerolog.Ctx(ctx).With().Stringer("reply_to_event_id", replyTo.MXID).Logger() + if relatesTo.GetReplyTo() == replyTo.MXID && len(relatesTo.InReplyTo.BeeperQuote) > 0 { + var content event.MessageEventContent + if err := json.Unmarshal(relatesTo.InReplyTo.BeeperQuote, &content); err != nil { + log.Warn().Err(err).Msg("Failed to parse quoted event content") + } else { + return mc.convertQuote(ctx, content) + } + } + evt, err := mc.Bridge.Bot.GetEvent(ctx, portal.MXID, replyTo.MXID) + if err != nil { + log.Debug().Err(err).Msg("Failed to fetch quoted event") + } else if evt == nil { + log.Debug().Msg("Quoted event not found") + } else if evt.RoomID != "" && evt.RoomID != portal.MXID { + log.Warn().Msg("Quoted event is in a different room") + } else if evt.Unsigned.RedactedBecause == nil { + return mc.convertQuote(ctx, *evt.Content.AsMessage()) + } + return &waE2E.Message{Conversation: new("")} +} + +func (mc *MessageConverter) convertQuote(ctx context.Context, content event.MessageEventContent) *waE2E.Message { + content.Mentions = &event.Mentions{} + content.RemoveReplyFallback() + content.RemovePerMessageProfileFallback() + text, _ := mc.parseText(ctx, &content) + return &waE2E.Message{Conversation: &text} +} + func (mc *MessageConverter) ToWhatsApp( ctx context.Context, client *whatsmeow.Client, @@ -107,7 +139,7 @@ func (mc *MessageConverter) ToWhatsApp( } message := &waE2E.Message{} - contextInfo := mc.generateContextInfo(ctx, replyTo, portal, content.BeeperDisappearingTimer, content.Mentions != nil && content.Mentions.Room) + contextInfo := mc.generateContextInfo(ctx, replyTo, content.RelatesTo, portal, content.BeeperDisappearingTimer, content.Mentions != nil && content.Mentions.Room) switch content.MsgType { case event.MsgText, event.MsgNotice, event.MsgEmote: diff --git a/pkg/msgconv/matrixpoll.go b/pkg/msgconv/matrixpoll.go index 5d5d014..fab8330 100644 --- a/pkg/msgconv/matrixpoll.go +++ b/pkg/msgconv/matrixpoll.go @@ -71,7 +71,7 @@ func (mc *MessageConverter) PollStartToWhatsApp( if maxAnswers >= len(content.PollStart.Answers) || maxAnswers < 0 { maxAnswers = 0 } - contextInfo := mc.generateContextInfo(ctx, replyTo, portal, nil, content.Mentions != nil && content.Mentions.Room) + contextInfo := mc.generateContextInfo(ctx, replyTo, content.RelatesTo, portal, nil, content.Mentions != nil && content.Mentions.Room) var question string question, contextInfo.MentionedJID = mc.msc1767ToWhatsApp(ctx, content.PollStart.Question, content.Mentions) if len(question) == 0 { From 977389392c7839330a4a1cfe49a74045a38d6f27 Mon Sep 17 00:00:00 2001 From: Matt Voska <3444419+voska@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:10:40 -0300 Subject: [PATCH 080/139] msgconv/from-whatsapp: add support for contact array messages (#961) --- pkg/msgconv/wa-contact.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/pkg/msgconv/wa-contact.go b/pkg/msgconv/wa-contact.go index 7ca3f8e..3807693 100644 --- a/pkg/msgconv/wa-contact.go +++ b/pkg/msgconv/wa-contact.go @@ -19,8 +19,10 @@ package msgconv import ( "context" "fmt" + "strings" "github.com/rs/zerolog" + "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow/proto/waE2E" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/event" @@ -65,11 +67,17 @@ func (mc *MessageConverter) convertContactMessage(ctx context.Context, msg *waE2 } func (mc *MessageConverter) convertContactsArrayMessage(ctx context.Context, msg *waE2E.ContactsArrayMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { - return &bridgev2.ConvertedMessagePart{ - Type: event.EventMessage, - Content: &event.MessageEventContent{ - MsgType: event.MsgNotice, - Body: "Contact array messages are not yet supported", - }, - }, msg.GetContextInfo() + name := msg.GetDisplayName() + if len(name) == 0 { + name = fmt.Sprintf("%d contacts", len(msg.GetContacts())) + } + vcards := make([]string, len(msg.GetContacts())) + for i, contact := range msg.GetContacts() { + vcards[i] = strings.TrimSpace(contact.GetVcard()) + } + part, _ := mc.convertContactMessage(ctx, &waE2E.ContactMessage{ + DisplayName: &name, + Vcard: ptr.Ptr(strings.Join(vcards, "\n")), + }) + return part, msg.GetContextInfo() } From aa86329e3717541b5db8ddd9e434019c8ce71070 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Wed, 7 Oct 2026 14:00:27 +0000 Subject: [PATCH 081/139] msgconv/from-matrix: use any available H.264 encoder for video conversion (#968) --- pkg/msgconv/from-matrix.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index 6f1d515..833813f 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -617,7 +617,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp( sourceFormat = "mov" } data, err = ffmpeg.ConvertBytes(ctx, data, ".mp4", []string{"-f", sourceFormat}, []string{ - "-pix_fmt", "yuv420p", "-c:v", "libx264", + "-pix_fmt", "yuv420p", "-c:v", "h264", "-b:v", "2M", "-filter:v", "crop='floor(in_w/2)*2:floor(in_h/2)*2'", }, mime) if err != nil { @@ -626,7 +626,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp( mime = "video/mp4" case "image/gif": data, err = ffmpeg.ConvertBytes(ctx, data, ".mp4", []string{"-f", "gif"}, []string{ - "-pix_fmt", "yuv420p", "-c:v", "libx264", "-movflags", "+faststart", + "-pix_fmt", "yuv420p", "-c:v", "h264", "-b:v", "2M", "-movflags", "+faststart", "-filter:v", "crop='floor(in_w/2)*2:floor(in_h/2)*2'", }, mime) if err != nil { From be8d93edc441b436db510f646cba2184cd0a7a3e Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Fri, 9 Oct 2026 15:23:35 +0100 Subject: [PATCH 082/139] msgconv/from-whatsapp: flag message history share notices as action messages (#971) --- pkg/msgconv/wa-misc.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/msgconv/wa-misc.go b/pkg/msgconv/wa-misc.go index cda33c5..275b132 100644 --- a/pkg/msgconv/wa-misc.go +++ b/pkg/msgconv/wa-misc.go @@ -199,8 +199,9 @@ func (mc *MessageConverter) convertMessageHistoryNotice(ctx context.Context, inf return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, Content: &event.MessageEventContent{ - MsgType: event.MsgNotice, - Body: body, + MsgType: event.MsgNotice, + Body: body, + BeeperActionMessage: &event.BeeperActionMessage{Type: "message_history_notice"}, }, }, msg.GetContextInfo() } From 45d51e3905ecd28dd6cb90e6ca0bd47512fc201b Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sat, 10 Oct 2026 10:47:11 +0000 Subject: [PATCH 083/139] login: fetch own avatar after new logins (#970) --- pkg/connector/login.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/connector/login.go b/pkg/connector/login.go index e5f7cd4..325bc55 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -13,6 +13,7 @@ import ( "github.com/rs/zerolog" "go.mau.fi/util/exsync" "go.mau.fi/util/jsontime" + "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" @@ -418,6 +419,7 @@ func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, er if err != nil { wl.Log.Warn().Err(err).Msg("Prekey upload wait failed") } + go c.syncGhost(c.JID.ToNonAD(), "login", ptr.Ptr("")) } return &bridgev2.LoginStep{ From 1fc894948c033c300529bd96a282a9e7159ef543 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 12 Oct 2026 12:34:18 +0000 Subject: [PATCH 084/139] handlewhatsapp: fix target message ID for delete for me in DMs (#975) --- pkg/connector/events.go | 14 +------------- pkg/connector/handlewhatsapp.go | 2 +- pkg/connector/id.go | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/pkg/connector/events.go b/pkg/connector/events.go index bd328c5..743492d 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -469,19 +469,7 @@ 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() - } - } - return sender + return evt.wa.getRealSender(evt.ChatID, evt.SenderID, evt.FromMe) } func (evt *WAMediaRetry) GetSender() bridgev2.EventSender { diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 2b30fdd..74606ab 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -721,7 +721,7 @@ func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.De func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool { chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID) - senderJID := wa.maybeConvertJIDToLID(ctx, evt.SenderJID) + senderJID := wa.getRealSender(chatJID, wa.maybeConvertJIDToLID(ctx, evt.SenderJID), evt.IsFromMe) return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventMessageRemove, diff --git a/pkg/connector/id.go b/pkg/connector/id.go index c0aa06b..43e949a 100644 --- a/pkg/connector/id.go +++ b/pkg/connector/id.go @@ -82,3 +82,18 @@ func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, jid types.JI } return jid } + +func (wa *WhatsAppClient) getRealSender(chat, sender types.JID, fromMe bool) types.JID { + if sender.IsEmpty() { + if fromMe { + if chat.Server == types.HiddenUserServer { + sender = wa.GetLID().ToNonAD() + } else { + sender = wa.JID.ToNonAD() + } + } else if chat.Server == types.DefaultUserServer || chat.Server == types.HiddenUserServer || chat.Server == types.BotServer { + sender = chat.ToNonAD() + } + } + return sender +} From 7fb697fb89b45a42e1e5ee7a668bc0ee6a24341a Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 13 Oct 2026 11:22:06 +0000 Subject: [PATCH 085/139] handlematrix: unfollow channels on leave and delete (#977) --- pkg/connector/handlematrix.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index ee3d70a..933c43a 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -439,6 +439,12 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg default: return nil, nil } + } else if portalJID.Server == types.NewsletterServer { + if msg.Type != bridgev2.Leave { + return nil, fmt.Errorf("cannot change channel members") + } + defer wa.mcTrack(msg, time.Now(), &retErr) + return nil, wa.Client.UnfollowNewsletter(ctx, portalJID) } changes := make([]types.JID, 1) @@ -690,7 +696,10 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg if err != nil { return err } - if chatJID.Server == types.GroupServer { + if chatJID.Server == types.NewsletterServer { + defer wa.mcTrack(msg, time.Now(), &retErr) + return wa.Client.UnfollowNewsletter(ctx, chatJID) + } else if chatJID.Server == types.GroupServer { memberInfo, err := wa.Main.Bridge.Matrix.GetMemberInfo(ctx, msg.Portal.MXID, wa.UserLogin.UserMXID) if err != nil { return fmt.Errorf("failed to get own member info: %w", err) From bf98cabad21ae37eacbe0c501b34abd011a59c8c Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 13 Oct 2026 11:46:49 +0000 Subject: [PATCH 086/139] chatinfo: don't overwrite mutes while regular_high recovery is pending (#978) --- pkg/connector/chatinfo.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 3984b5f..d26aeae 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -10,6 +10,7 @@ import ( "go.mau.fi/util/jsontime" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/appstate" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" "maunium.net/go/mautrix/bridgev2" @@ -116,9 +117,16 @@ func (wa *WhatsAppClient) applyChatSettings(ctx context.Context, chatID types.JI return } } - info.UserLocal = &bridgev2.UserLocalPortalInfo{ - MutedUntil: ptr.Ptr(chat.MutedUntil), + wa.appStateRecoveryLock.Lock() + _, muteStateBroken := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).AppStateRecoveryAttempted[appstate.WAPatchRegularHigh] + wa.appStateRecoveryLock.Unlock() + userLocal := &bridgev2.UserLocalPortalInfo{} + if !muteStateBroken { + userLocal.MutedUntil = ptr.Ptr(chat.MutedUntil) + } else if info.UserLocal != nil { + userLocal.MutedUntil = info.UserLocal.MutedUntil } + info.UserLocal = userLocal if chat.Pinned { info.UserLocal.Tag = ptr.Ptr(wa.Main.Config.PinnedTag) } else if chat.Archived { From 73b70f2753a4a0fbede6f2a1da7946259097769a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 13 Oct 2026 17:32:09 +0300 Subject: [PATCH 087/139] chatinfo,handlematrix: allow encrypted poll votes in read-only groups Permissions are now checked by whatsmeow to return appropriate errors --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/chatinfo.go | 16 ++++++++++------ pkg/connector/events.go | 1 + pkg/connector/handlematrix.go | 5 +++++ pkg/msgconv/from-matrix.go | 1 - 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index b6398d4..334a3bd 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/yuin/goldmark v1.8.6 go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20261006124319-9399289b022b + go.mau.fi/whatsmeow v0.0.0-20261013142727-4cf8bc8e0aeb golang.org/x/image v0.46.0 golang.org/x/net v0.59.0 golang.org/x/sync v0.23.0 diff --git a/go.sum b/go.sum index 5854082..e5f286d 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcU go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= 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-20261006124319-9399289b022b h1:sxHC8T2zT69QJmFx6zuB3KuDu+49KFf28bszKtv8vXg= -go.mau.fi/whatsmeow v0.0.0-20261006124319-9399289b022b/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= +go.mau.fi/whatsmeow v0.0.0-20261013142727-4cf8bc8e0aeb h1:yRvCcaQwqHMpUrV9Cn48QOw/YSsV1BlXCs00BTRz0Ks= +go.mau.fi/whatsmeow v0.0.0-20261013142727-4cf8bc8e0aeb/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= 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.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index d26aeae..61b9e74 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -276,6 +276,8 @@ func setTopicID(id, topic string) bridgev2.ExtraUpdater[*bridgev2.Portal] { func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupInfo) *bridgev2.ChatInfo { sendEventPL := defaultPL + // Community announcement groups allow non-admins to send threaded replies. + // Power levels can't represent that, but the power level is checked separately when handling messages. if info.IsAnnounce && !info.IsDefaultSubGroup { sendEventPL = adminPL } @@ -306,6 +308,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn event.StateRoomName: metaChangePL, event.StateRoomAvatar: metaChangePL, event.StateTopic: metaChangePL, + event.EventEncrypted: defaultPL, // Need to always allow encrypted events for poll responses event.EventReaction: defaultPL, event.EventRedaction: defaultPL, event.EventUnstablePollResponse: defaultPL, @@ -572,12 +575,13 @@ 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 support poll votes in channels + //event.EventUnstablePollResponse: defaultPL, }, }, }, diff --git a/pkg/connector/events.go b/pkg/connector/events.go index 743492d..481d0d7 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -539,6 +539,7 @@ func (evt *WAMediaRetry) ConvertEdit(ctx context.Context, portal *bridgev2.Porta log.Debug().Msg("Got error response in media retry notification, but response also contains a new download URL - trying to download") } } + log.Trace().Any("retry_content", retryData).Msg("Decrypted successful media retry response") err = evt.wa.mediaRetryLock.Acquire(ctx, 1) if err != nil { return nil, fmt.Errorf("failed to acquire media retry lock: %w", err) diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 933c43a..fc5760e 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -106,6 +106,7 @@ func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2 var ErrBroadcastSendDisabled = bridgev2.WrapErrorInStatus(errors.New("sending status messages is disabled")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported) var ErrBroadcastReactionUnsupported = bridgev2.WrapErrorInStatus(errors.New("reacting to status messages is not currently supported")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported) +var ErrNotCommunityAdmin = bridgev2.WrapErrorInStatus(errors.New("only community admins can send announcements")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusNoPermission) func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage, waMsg *waE2E.Message, req *whatsmeow.SendRequestExtra) (*bridgev2.MatrixMessageResponse, error) { if req == nil { @@ -116,6 +117,7 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg } else { req.ID = wa.Client.GenerateMessageID() } + req.CheckPermissions = true chatJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { @@ -134,6 +136,9 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg zerolog.Ctx(ctx).Trace().Any("payload", waMsg).Msg("Outgoing message payload") resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req) if err != nil { + if errors.Is(err, whatsmeow.ErrNoPermissionInGroup) { + return nil, ErrNotCommunityAdmin + } return nil, err } var pickedMessageID networkid.MessageID diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index 833813f..192b8a6 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -193,7 +193,6 @@ func (mc *MessageConverter) ToWhatsApp( DeprecatedLIDSession: ptr.Ptr(false), } } else { - // TODO check permissions? message.MessageContextInfo = &waE2E.MessageContextInfo{ MessageSecret: random.Bytes(32), } From abf1940af85952912487a87671c82e895d8f574a Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Wed, 14 Oct 2026 22:39:42 +0100 Subject: [PATCH 088/139] backfill: skip fake anchor messages in on-demand backwards backfill (#974) Co-authored-by: Claude Opus 5.5 --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/backfill.go | 16 +++++++++++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 334a3bd..730d968 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( golang.org/x/sync v0.23.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.31.1-0.20261005174338-57e1ea5dd74d + maunium.net/go/mautrix v0.31.1-0.20261014100333-d27bb75b1649 ) require ( diff --git a/go.sum b/go.sum index e5f286d..fcbdc22 100644 --- a/go.sum +++ b/go.sum @@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.31.1-0.20261005174338-57e1ea5dd74d h1:sOdF5AF9MiU2mudfMBgQC79FJrhBBDbUkxQCi0T2uHI= -maunium.net/go/mautrix v0.31.1-0.20261005174338-57e1ea5dd74d/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk= +maunium.net/go/mautrix v0.31.1-0.20261014100333-d27bb75b1649 h1:oL4IWgkubcKHVyvTr38FzkQ24EeDZuKTpb1ZSxLnKzU= +maunium.net/go/mautrix v0.31.1-0.20261014100333-d27bb75b1649/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk= diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 85f4499..e582ad1 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -706,9 +706,19 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID if params.AnchorMessage == nil { return nil, fmt.Errorf("anchor message is required to fetch messages from phone") } - parsed, err := waid.ParseMessageID(params.AnchorMessage.ID) + anchor := params.AnchorMessage + parsed, err := waid.ParseMessageID(anchor.ID) if err != nil { - return nil, fmt.Errorf("failed to parse anchor message ID: %w", err) + anchor, err = wa.Main.Bridge.DB.Message.GetFirstPortalMessageExcludingIDPattern(ctx, params.Portal.PortalKey, "fake:%") + if err != nil { + return nil, fmt.Errorf("failed to get first non-fake portal message: %w", err) + } else if anchor == nil { + return &bridgev2.FetchMessagesResponse{}, nil + } + parsed, err = waid.ParseMessageID(anchor.ID) + if err != nil { + return nil, fmt.Errorf("failed to parse anchor message ID: %w", err) + } } msgID := wa.Client.GenerateMessageID() @@ -720,7 +730,7 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID IsGroup: parsed.Chat.Server == types.GroupServer, }, ID: parsed.ID, - Timestamp: params.AnchorMessage.Timestamp, + Timestamp: anchor.Timestamp, }, 50) zerolog.Ctx(ctx).Debug(). Str("request_msg_id", msgID). From 09329681068f38b962ee7bddf65a2488a4130df9 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 15 Oct 2026 13:36:27 +0300 Subject: [PATCH 089/139] dependencies: update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 730d968..989ec73 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/yuin/goldmark v1.8.6 go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20261013142727-4cf8bc8e0aeb + go.mau.fi/whatsmeow v0.0.0-20261015103503-3142697f59a2 golang.org/x/image v0.46.0 golang.org/x/net v0.59.0 golang.org/x/sync v0.23.0 diff --git a/go.sum b/go.sum index fcbdc22..47fad46 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcU go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= 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-20261013142727-4cf8bc8e0aeb h1:yRvCcaQwqHMpUrV9Cn48QOw/YSsV1BlXCs00BTRz0Ks= -go.mau.fi/whatsmeow v0.0.0-20261013142727-4cf8bc8e0aeb/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= +go.mau.fi/whatsmeow v0.0.0-20261015103503-3142697f59a2 h1:SJbKhrV30ZDjQA7dpeqm4565LTIQs5AByuMmy8gbLnk= +go.mau.fi/whatsmeow v0.0.0-20261015103503-3142697f59a2/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= 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.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= From 1f7131c2eef8c40a32d0e39f33398b29e2675aef Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Fri, 16 Oct 2026 11:05:18 +0000 Subject: [PATCH 090/139] mediarequest: send pending requests at the first scheduled time (#981) --- pkg/connector/mediarequest.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/connector/mediarequest.go b/pkg/connector/mediarequest.go index 3806196..6a30cf6 100644 --- a/pkg/connector/mediarequest.go +++ b/pkg/connector/mediarequest.go @@ -88,6 +88,7 @@ func (wa *WhatsAppClient) mediaRequestLoop(ctx context.Context) { return case <-time.After(startIn): } + wa.sendMediaRequests(ctx) ticker := time.NewTicker(24 * time.Hour) defer ticker.Stop() for { From 1067291a89f4949887b328104005abe4f7163ddf Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 16 Oct 2026 18:03:24 +0300 Subject: [PATCH 091/139] Bump version to v26.10 --- CHANGELOG.md | 25 +++++++++++++++++ cmd/mautrix-whatsapp/main.go | 2 +- go.mod | 28 +++++++++---------- go.sum | 52 ++++++++++++++++++------------------ 4 files changed, 66 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5dd173..3002f92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +# v26.10 + +* Added support for new rich response messages used by the Muse AI bot. +* Added support for bridging custom profile set for Muse. +* Added automatic one-time resync of keys needed to decrypt Muse messages. +* Added support for blocking and unblocking users on WhatsApp. +* Added support for contact array messages (thanks to [@voska] in [#961]). +* Added partial support for filling quoted message when sending replies to + WhatsApp. +* Changed undecryptable message resends to only bridge as edits and never as new + messages, to avoid races if the message was deleted before the resend. +* Changed login to wait for OTKs to be successfully uploaded, as the login will + fail if the bridge is stopped before that point. +* Changed chat info syncing to not bridge mute status if app state sync is + failing. +* Changed power levels to allow poll votes in read-only groups even if the room + is encrypted. +* Fixed backfilled media retries not getting sent if configured for local time. +* Fixed "delete for me" from WhatsApp not being bridged correctly in DMs. +* Fixed very long WhatsApp messages not being bridged. +* Fixed document message caption edits not being handled correctly. + +[@voska]: https://github.com/voska +[#961]: https://github.com/mautrix/whatsapp/pull/961 + # v26.09 * Bumped minimum Go version to 1.26. diff --git a/cmd/mautrix-whatsapp/main.go b/cmd/mautrix-whatsapp/main.go index a1d85c3..ded185c 100644 --- a/cmd/mautrix-whatsapp/main.go +++ b/cmd/mautrix-whatsapp/main.go @@ -18,7 +18,7 @@ var m = mxmain.BridgeMain{ Name: "mautrix-whatsapp", URL: "https://github.com/mautrix/whatsapp", Description: "A Matrix-WhatsApp puppeting bridge.", - Version: "26.09", + Version: "26.10", SemCalVer: true, Connector: &connector.WhatsAppConnector{}, } diff --git a/go.mod b/go.mod index 989ec73..0c70c3d 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module go.mau.fi/mautrix-whatsapp go 1.26.0 -toolchain go1.27.1 +toolchain go1.27.2 tool go.mau.fi/util/cmd/maubuild @@ -10,17 +10,17 @@ require ( github.com/google/uuid v1.6.0 github.com/lib/pq v1.12.3 github.com/rs/zerolog v1.35.1 - github.com/tidwall/gjson v1.19.0 + github.com/tidwall/gjson v1.20.0 github.com/yuin/goldmark v1.8.6 - go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 + go.mau.fi/util v0.11.0 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20261015103503-3142697f59a2 - golang.org/x/image v0.46.0 - golang.org/x/net v0.59.0 - golang.org/x/sync v0.23.0 + go.mau.fi/whatsmeow v0.0.0-20261016130820-0b0a1cdac3e2 + golang.org/x/image v0.47.0 + golang.org/x/net v0.61.0 + golang.org/x/sync v0.24.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.31.1-0.20261014100333-d27bb75b1649 + maunium.net/go/mautrix v0.32.0 ) require ( @@ -33,7 +33,7 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.52 // indirect - github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 // indirect + github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b // 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 @@ -43,11 +43,11 @@ require ( github.com/vektah/gqlparser/v2 v2.5.27 // indirect go.mau.fi/libsignal v0.2.2 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect - golang.org/x/crypto v0.57.0 // indirect - golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect - golang.org/x/mod v0.41.0 // indirect - golang.org/x/sys v0.48.0 // indirect - golang.org/x/text v0.42.0 // indirect + golang.org/x/crypto v0.58.0 // indirect + golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e // indirect + golang.org/x/mod v0.42.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/text v0.43.0 // indirect 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 47fad46..576ec24 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U= github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= -github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 h1:lcWAnrqr2nNfDiArwFNHCE4787Mw2tCdVSOXCru0/0E= -github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b h1:OzNsuVdSWGwXvWKTtChx9ve89k4cFJ6NxYM4Aw4/f+E= +github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -51,8 +51,8 @@ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDq github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= -github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +github.com/tidwall/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto= +github.com/tidwall/gjson v1.20.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= github.com/tidwall/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,31 +67,31 @@ github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU= go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0= -go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8 h1:PxaO4u8ti7Eef1WKPAINXrcUkqKshE+n239UtbbJOn4= -go.mau.fi/util v0.10.2-0.20260918225449-a4c0d5b86aa8/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= +go.mau.fi/util v0.11.0 h1:JNEpkVRxfTdJUC0DIGKiRlLKzl4G/qLX/eVzoP7QuwY= +go.mau.fi/util v0.11.0/go.mod h1:04AEFVsc4zNdIkDC1gkdqbg/nZFolHY50pyXh2DSy7s= go.mau.fi/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-20261015103503-3142697f59a2 h1:SJbKhrV30ZDjQA7dpeqm4565LTIQs5AByuMmy8gbLnk= -go.mau.fi/whatsmeow v0.0.0-20261015103503-3142697f59a2/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= +go.mau.fi/whatsmeow v0.0.0-20261016130820-0b0a1cdac3e2 h1:+EhsWM5VeE5Vu3KRlk3PmYAY0lEYkbhI5W4mTmM9X/Q= +go.mau.fi/whatsmeow v0.0.0-20261016130820-0b0a1cdac3e2/go.mod h1:iY4lm61nTLWsNiIE1EmHUZwhKGBeoV4dhnVE2uMtNoQ= 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.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= -golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= -golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= -golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ= -golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= -golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= -golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= -golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= -golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= -golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= -golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= +golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= +golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e h1:HgI6W1OolKK6tcV74is+rl9SMT0njsHlp0TbQiGXI/A= +golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e/go.mod h1:DBoxwWhjaW8DJJw/YeM4zpeOKMC1FMXSblsYy+1mFSw= +golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo= +golang.org/x/image v0.47.0/go.mod h1:IkN5o4A7vq55vXN8FWjjnhg9Rs2GOtffqsSi9mWAKYE= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.31.1-0.20261014100333-d27bb75b1649 h1:oL4IWgkubcKHVyvTr38FzkQ24EeDZuKTpb1ZSxLnKzU= -maunium.net/go/mautrix v0.31.1-0.20261014100333-d27bb75b1649/go.mod h1:t97J+6KmbmeoepWOunBgpu1xIvuBpI36ebBqIbYT8Jk= +maunium.net/go/mautrix v0.32.0 h1:xuAZzzyWce0pFkBhV/MCLFiJdVs2rFkY3Eep7hNng4I= +maunium.net/go/mautrix v0.32.0/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= From c8e38f1dc5093c7d30318719a60be0a8aeb00446 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Fri, 16 Oct 2026 12:39:40 -0400 Subject: [PATCH 092/139] handlewhatsapp: ignore album headers without a reply (#982) --- pkg/connector/wamsgtype.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/connector/wamsgtype.go b/pkg/connector/wamsgtype.go index efdf679..2b1458b 100644 --- a/pkg/connector/wamsgtype.go +++ b/pkg/connector/wamsgtype.go @@ -142,7 +142,10 @@ func getMessageType(waMsg *waE2E.Message) string { case waMsg.StickerPackMessage != nil: return "sticker pack" case waMsg.AlbumMessage != nil: - return "album" // or maybe these should be ignored? + if waMsg.GetAlbumMessage().GetContextInfo().GetStanzaID() == "" { + return "ignore" + } + return "album" case waMsg.SendPaymentMessage != nil, waMsg.RequestPaymentMessage != nil, waMsg.DeclinePaymentRequestMessage != nil, waMsg.CancelPaymentRequestMessage != nil, waMsg.PaymentInviteMessage != nil: From 46afb8f781c616514962a4478240844a44c2f1d3 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Fri, 16 Oct 2026 19:35:52 -0400 Subject: [PATCH 093/139] backfill: don't repeat on-demand history sync requests while one is in flight (#980) --- pkg/connector/backfill.go | 40 +++++++++++++++++++++++++++++++++++++++ pkg/connector/client.go | 5 +++++ 2 files changed, 45 insertions(+) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index e582ad1..f1fc56e 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -147,6 +147,7 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, return } if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND { + wa.finishOnDemandHistorySyncRequest(evt.GetPeerDataRequestSessionID()) wa.handleOnDemandHistorySync(ctx, blob) if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil { log.Err(err).Msg("Failed to delete queued on-demand history sync notification") @@ -722,6 +723,12 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID } msgID := wa.Client.GenerateMessageID() + if !wa.startOnDemandHistorySyncRequest(ctx, portalJID, msgID) { + return &bridgev2.FetchMessagesResponse{ + HasMore: true, + Pending: true, + }, nil + } reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{ MessageSource: types.MessageSource{ Chat: portalJID, @@ -743,6 +750,9 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID Peer: true, }) if err != nil { + wa.onDemandHistorySyncLock.Lock() + delete(wa.onDemandHistorySyncRequests, portalJID) + wa.onDemandHistorySyncLock.Unlock() return nil, fmt.Errorf("failed to send history sync request: %w", err) } return &bridgev2.FetchMessagesResponse{ @@ -751,6 +761,36 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID }, nil } +type onDemandHistorySyncRequest struct { + ID types.MessageID + SentAt time.Time +} + +func (wa *WhatsAppClient) startOnDemandHistorySyncRequest(ctx context.Context, portalJID types.JID, msgID types.MessageID) bool { + wa.onDemandHistorySyncLock.Lock() + defer wa.onDemandHistorySyncLock.Unlock() + if req, inFlight := wa.onDemandHistorySyncRequests[portalJID]; inFlight { + zerolog.Ctx(ctx).Debug(). + Str("in_flight_request_msg_id", req.ID). + Stringer("portal_jid", portalJID). + Time("sent_at", req.SentAt). + Msg("Not sending history sync request, previous request is still in flight") + return false + } + wa.onDemandHistorySyncRequests[portalJID] = onDemandHistorySyncRequest{ID: msgID, SentAt: time.Now()} + return true +} + +func (wa *WhatsAppClient) finishOnDemandHistorySyncRequest(sessionID string) { + wa.onDemandHistorySyncLock.Lock() + defer wa.onDemandHistorySyncLock.Unlock() + for chatJID, req := range wa.onDemandHistorySyncRequests { + if req.ID == sessionID { + delete(wa.onDemandHistorySyncRequests, chatJID) + } + } +} + func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *waHistorySync.HistorySync) { if len(blob.GetConversations()) > 1 { zerolog.Ctx(ctx).Warn(). diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 61fceec..cd52be2 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -56,6 +56,8 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. pushNamesSynced: exsync.NewEvent(), appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time), + onDemandHistorySyncRequests: make(map[types.JID]onDemandHistorySyncRequest), + disableNewsletter: store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS, } login.Client = w @@ -132,6 +134,9 @@ type WhatsAppClient struct { wasaResyncLock sync.Mutex museProfileLock sync.Mutex lastMuseProfileRequest time.Time + + onDemandHistorySyncLock sync.Mutex + onDemandHistorySyncRequests map[types.JID]onDemandHistorySyncRequest } var ( From 054a7dee77b571b7716a3b0d02ef55adaa4c8c22 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sun, 18 Oct 2026 12:35:50 +0000 Subject: [PATCH 094/139] directmedia: use LID for sender in media IDs (#983) --- pkg/connector/directmedia.go | 14 ++++++++++++++ pkg/waid/id.go | 8 ++++++-- pkg/waid/mediaid.go | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index 8ed94ce..b86f7dc 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -34,6 +34,7 @@ import ( "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waMmsRetry" + "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" "maunium.net/go/mautrix" "maunium.net/go/mautrix/bridgev2" @@ -173,6 +174,9 @@ func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, par func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) { msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String()) + if err == nil && msg == nil && parsedID.Message.Chat.Server == types.HiddenUserServer && parsedID.Message.Sender.Server == types.DefaultUserServer { + msg, err = wa.getDirectMediaMessageWithLIDSender(ctx, parsedID) + } if err != nil { return nil, fmt.Errorf("failed to get message: %w", err) } else if msg == nil { @@ -215,6 +219,16 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par return wa.makeDirectMediaResponse(ctx, waClient, keys, keys.MimeType, msg.ID, keys, params) } +func (wa *WhatsAppConnector) getDirectMediaMessageWithLIDSender(ctx context.Context, parsedID *waid.ParsedMediaID) (*database.Message, error) { + lid, err := wa.DeviceStore.LIDMap.GetLIDForPN(ctx, parsedID.Message.Sender) + if err != nil { + return nil, fmt.Errorf("failed to get LID for sender: %w", err) + } else if lid.IsEmpty() { + return nil, nil + } + return wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, waid.MakeMessageID(parsedID.Message.Chat, lid, parsedID.Message.ID)) +} + func (wa *WhatsAppConnector) makeDirectMediaResponse( ctx context.Context, waClient *WhatsAppClient, diff --git a/pkg/waid/id.go b/pkg/waid/id.go index 7f8fe5b..6edbcb8 100644 --- a/pkg/waid/id.go +++ b/pkg/waid/id.go @@ -84,10 +84,14 @@ func MakeMessageID(chat, sender types.JID, id types.MessageID) networkid.Message } func MakeMessageIDWithAltSender(chat, sender, altSender types.JID, id types.MessageID) networkid.MessageID { + return MakeMessageID(chat, pickMessageIDSender(chat, sender, altSender), id) +} + +func pickMessageIDSender(chat, sender, altSender types.JID) types.JID { if chat.Server == types.HiddenUserServer && sender.Server == types.DefaultUserServer && altSender.Server == types.HiddenUserServer { - sender = altSender + return altSender } - return MakeMessageID(chat, sender, id) + return sender } func MakeFakeMessageID(chat, sender types.JID, data string) networkid.MessageID { diff --git a/pkg/waid/mediaid.go b/pkg/waid/mediaid.go index a0ac2d9..913a23b 100644 --- a/pkg/waid/mediaid.go +++ b/pkg/waid/mediaid.go @@ -39,7 +39,7 @@ const ( func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID, version []byte) networkid.MediaID { compactChat := compactJID(messageInfo.Chat.ToNonAD()) - compactSender := compactJID(messageInfo.Sender.ToNonAD()) + compactSender := compactJID(pickMessageIDSender(messageInfo.Chat, messageInfo.Sender, messageInfo.SenderAlt).ToNonAD()) receiverID := compactJID(ParseUserLoginID(receiver, 0)) var compactID []byte if idOverride != "" { From c998fd8858ce767ff5b01de71c9d8a4c6669cab8 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Thu, 22 Oct 2026 14:24:34 +0200 Subject: [PATCH 095/139] connector: map WhatsApp errors to message statuses and HTTP errors (#985) --- .bridgelint.yaml | 1 + pkg/connector/errors.go | 107 ++++++++++++++++++++++++++++++++ pkg/connector/handlematrix.go | 54 ++++++++-------- pkg/connector/handlewhatsapp.go | 1 + pkg/connector/login.go | 10 +-- pkg/connector/proxy.go | 15 +++-- pkg/connector/startchat.go | 6 +- pkg/msgconv/matrixpoll.go | 4 +- 8 files changed, 156 insertions(+), 42 deletions(-) create mode 100644 .bridgelint.yaml create mode 100644 pkg/connector/errors.go diff --git a/.bridgelint.yaml b/.bridgelint.yaml new file mode 100644 index 0000000..ea2b48b --- /dev/null +++ b/.bridgelint.yaml @@ -0,0 +1 @@ +code_format_allowlist: ["wa-connect-failure-%d"] diff --git a/pkg/connector/errors.go b/pkg/connector/errors.go new file mode 100644 index 0000000..1ee1b20 --- /dev/null +++ b/pkg/connector/errors.go @@ -0,0 +1,107 @@ +// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2026 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package connector + +import ( + "context" + "errors" + "fmt" + "net/http" + + "go.mau.fi/whatsmeow" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/event" +) + +// wrapConversionError turns a failure to convert a Matrix event into a message status. Nothing has been +// sent to WhatsApp at that point, so the failure is certain. +func wrapConversionError(err error) error { + if err == nil || errors.As(err, &bridgev2.MessageStatus{}) { + return err + } + return bridgev2.WrapErrorInStatus(err). + WithMessage("failed to convert message for WhatsApp"). + WithIsCertain(true). + WithSendNotice(true) +} + +// wrapWhatsAppError turns an error from a whatsmeow request made while handling a Matrix event into a +// message status. +func wrapWhatsAppError(err error) error { + if err == nil || errors.As(err, &bridgev2.MessageStatus{}) { + return err + } + var iqErr *whatsmeow.IQError + var disconnectErr *whatsmeow.DisconnectedError + switch { + case errors.Is(err, whatsmeow.ErrNotConnected), errors.Is(err, whatsmeow.ErrClientIsNil): + return bridgev2.WrapErrorInStatus(err). + WithMessage("not connected to WhatsApp"). + WithIsCertain(true). + WithSendNotice(true) + case errors.Is(err, whatsmeow.ErrMessageTimedOut), errors.Is(err, whatsmeow.ErrIQTimedOut), + errors.Is(err, context.DeadlineExceeded), errors.As(err, &disconnectErr): + return bridgev2.WrapErrorInStatus(err). + WithMessage("no response from WhatsApp"). + WithIsCertain(false). + WithSendNotice(true) + case errors.Is(err, whatsmeow.ErrIQRateOverLimit): + return bridgev2.WrapErrorInStatus(err). + WithMessage("rate limited by WhatsApp"). + WithErrorReason(event.MessageStatusNetworkError). + WithIsCertain(true). + WithSendNotice(true) + case errors.Is(err, whatsmeow.ErrIQForbidden), errors.Is(err, whatsmeow.ErrIQNotAuthorized): + return bridgev2.WrapErrorInStatus(err). + WithMessage("you don't have permission to do that on WhatsApp"). + WithErrorReason(event.MessageStatusNoPermission). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + case errors.Is(err, whatsmeow.ErrServerReturnedError), errors.As(err, &iqErr): + return bridgev2.WrapErrorInStatus(err). + WithMessage("WhatsApp returned an error"). + WithIsCertain(true). + WithSendNotice(true) + default: + return bridgev2.WrapErrorInStatus(err). + WithMessage("unexpected error from WhatsApp"). + WithSendNotice(true) + } +} + +// wrapWhatsAppRespError turns an error from a whatsmeow request made during login or provisioning into +// an HTTP error, keeping the original error in the chain. Clients retry 429 and 502-504 automatically, +// which isn't wanted for provisioning requests, so those statuses aren't used. +func wrapWhatsAppRespError(err error) error { + var mapped bridgev2.RespError + var disconnectErr *whatsmeow.DisconnectedError + switch { + case errors.Is(err, whatsmeow.ErrIQRateOverLimit): + mapped = bridgev2.RespError(mautrix.MLimitExceeded.WithMessage("Rate limited by WhatsApp. Try again later.").WithStatus(http.StatusBadRequest)) + case errors.Is(err, whatsmeow.ErrNotConnected), errors.Is(err, whatsmeow.ErrClientIsNil), errors.As(err, &disconnectErr): + mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("Not connected to WhatsApp")) + case errors.Is(err, whatsmeow.ErrIQTimedOut), errors.Is(err, context.DeadlineExceeded): + mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("WhatsApp didn't respond in time")) + case errors.Is(err, whatsmeow.ErrIQForbidden), errors.Is(err, whatsmeow.ErrIQNotAuthorized): + mapped = bridgev2.RespError(mautrix.MForbidden.WithMessage("WhatsApp denied the request")) + default: + mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("Request to WhatsApp failed")) + } + return fmt.Errorf("%w: %w", mapped.WithInternalError(err), err) +} diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index fc5760e..fec40c3 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -69,7 +69,7 @@ var ( func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (result *bridgev2.MatrixMessageResponse, retErr error) { waMsg, optionMap, err := wa.Main.MsgConv.PollStartToWhatsApp(ctx, msg.Content, msg.ReplyTo, msg.Portal) if err != nil { - return nil, fmt.Errorf("failed to convert poll vote: %w", err) + return nil, wrapConversionError(fmt.Errorf("failed to convert poll start: %w", err)) } defer wa.mcTrack(msg, time.Now(), &retErr) resp, err := wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil) @@ -89,7 +89,7 @@ func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridge func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (result *bridgev2.MatrixMessageResponse, retErr error) { waMsg, err := wa.Main.MsgConv.PollVoteToWhatsApp(ctx, wa.Client, msg.Content, msg.VoteTo) if err != nil { - return nil, fmt.Errorf("failed to convert poll vote: %w", err) + return nil, wrapConversionError(fmt.Errorf("failed to convert poll vote: %w", err)) } defer wa.mcTrack(msg, time.Now(), &retErr) return wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil) @@ -98,15 +98,15 @@ func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (result *bridgev2.MatrixMessageResponse, retErr error) { waMsg, req, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, msg.Event, msg.Content, msg.ReplyTo, msg.ThreadRoot, msg.Portal) if err != nil { - return nil, fmt.Errorf("failed to convert message: %w", err) + return nil, wrapConversionError(fmt.Errorf("failed to convert message: %w", err)) } defer wa.mcTrack(msg, time.Now(), &retErr) return wa.handleConvertedMatrixMessage(ctx, msg, waMsg, req) } -var ErrBroadcastSendDisabled = bridgev2.WrapErrorInStatus(errors.New("sending status messages is disabled")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported) -var ErrBroadcastReactionUnsupported = bridgev2.WrapErrorInStatus(errors.New("reacting to status messages is not currently supported")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported) -var ErrNotCommunityAdmin = bridgev2.WrapErrorInStatus(errors.New("only community admins can send announcements")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusNoPermission) +var ErrBroadcastSendDisabled = bridgev2.WrapErrorInStatus(errors.New("sending status messages is disabled")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) +var ErrBroadcastReactionUnsupported = bridgev2.WrapErrorInStatus(errors.New("reacting to status messages is not currently supported")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) +var ErrNotCommunityAdmin = bridgev2.WrapErrorInStatus(errors.New("only community admins can send announcements")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusNoPermission).WithStatus(event.MessageStatusFail) func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage, waMsg *waE2E.Message, req *whatsmeow.SendRequestExtra) (*bridgev2.MatrixMessageResponse, error) { if req == nil { @@ -139,7 +139,7 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg if errors.Is(err, whatsmeow.ErrNoPermissionInGroup) { return nil, ErrNotCommunityAdmin } - return nil, err + return nil, wrapWhatsAppError(err) } var pickedMessageID networkid.MessageID if resp.Sender == wa.GetLID() { @@ -204,7 +204,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup { reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(ctx, wa.Client, messageID), reactionMsg.ReactionMessage) if err != nil { - return nil, fmt.Errorf("failed to encrypt reaction: %w", err) + return nil, wrapWhatsAppError(fmt.Errorf("failed to encrypt reaction: %w", err)) } reactionMsg.ReactionMessage = nil req.Meta = &types.MsgMetaInfo{ @@ -218,7 +218,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev Metadata: &waid.ReactionMetadata{ SenderDeviceID: wa.JID.Device, }, - }, err + }, wrapWhatsAppError(err) } func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) (retErr error) { @@ -248,7 +248,7 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b defer wa.mcTrack(msg, time.Now(), &retErr) resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra) zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response") - return err + return wrapWhatsAppError(err) } func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) (retErr error) { @@ -273,7 +273,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M waMsg, _, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, edit.Event, edit.Content, nil, nil, edit.Portal) if err != nil { - return fmt.Errorf("failed to convert message: %w", err) + return wrapConversionError(fmt.Errorf("failed to convert message: %w", err)) } defer wa.mcTrack(edit, time.Now(), &retErr) @@ -288,7 +288,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M ID: editID, }) log.Trace().Any("response", resp).Msg("WhatsApp edit response") - return err + return wrapWhatsAppError(err) } func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) (retErr error) { @@ -313,7 +313,7 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br resp, err := wa.Client.SendMessage(ctx, portalJID, revokeMessage, extra) log.Trace().Any("response", resp).Msg("WhatsApp delete response") - return err + return wrapWhatsAppError(err) } func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) { @@ -362,7 +362,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt * log.Err(err).Strs("ids", ids).Msg("Failed to mark messages as read") } } - return err + return wrapWhatsAppError(err) } func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) (retErr error) { @@ -393,7 +393,7 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2. zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set presence on typing") } } - return wa.Client.SendChatPresence(ctx, portalJID, chatPresence, mediaPresence) + return wrapWhatsAppError(wa.Client.SendChatPresence(ctx, portalJID, chatPresence, mediaPresence)) } var errUnsupportedDisappearingTimer = bridgev2.WrapErrorInStatus(errors.New("invalid value for disappearing timer")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true) @@ -414,7 +414,7 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg settingTS := time.UnixMilli(msg.Event.Timestamp) err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS) if err != nil { - return false, err + return false, wrapWhatsAppError(err) } msg.Portal.Metadata.(*waid.PortalMetadata).DisappearingTimerSetAt = settingTS.Unix() msg.Portal.Disappear = database.DisappearingSetting{ @@ -449,7 +449,7 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg return nil, fmt.Errorf("cannot change channel members") } defer wa.mcTrack(msg, time.Now(), &retErr) - return nil, wa.Client.UnfollowNewsletter(ctx, portalJID) + return nil, wrapWhatsAppError(wa.Client.UnfollowNewsletter(ctx, portalJID)) } changes := make([]types.JID, 1) @@ -480,7 +480,7 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg defer wa.mcTrack(msg, time.Now(), &retErr) resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action) if err != nil { - return nil, err + return nil, wrapWhatsAppError(err) } else if len(resp) == 0 { return nil, fmt.Errorf("no response for participant change") } else if resp[0].Error != 0 { @@ -506,7 +506,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev defer wa.mcTrack(msg, time.Now(), &retErr) err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name) if err != nil { - return false, err + return false, wrapWhatsAppError(err) } msg.Portal.Name = msg.Content.Name @@ -530,7 +530,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic) if err != nil { - return false, err + return false, wrapWhatsAppError(err) } msg.Portal.Topic = msg.Content.Topic @@ -566,7 +566,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridg defer wa.mcTrack(msg, time.Now(), &retErr) avatarID, err := wa.Client.SetGroupPhoto(ctx, portalJID, data) if err != nil { - return false, err + return false, wrapWhatsAppError(err) } msg.Portal.AvatarMXC = msg.Content.URL @@ -595,7 +595,7 @@ func convertRoomAvatar(data []byte) ([]byte, error) { if isCorrectSize && imageType == "jpeg" { return data, nil } else if len(data) > 10*1024*1024 || width > 12000 || height > 12000 { - return nil, fmt.Errorf("avatar is too large for re-encoding") + return nil, fmt.Errorf("%w: avatar is too large for re-encoding", bridgev2.ErrMediaTooLarge) } img, _, err := image.Decode(bytes.NewReader(data)) @@ -703,7 +703,7 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg } if chatJID.Server == types.NewsletterServer { defer wa.mcTrack(msg, time.Now(), &retErr) - return wa.Client.UnfollowNewsletter(ctx, chatJID) + return wrapWhatsAppError(wa.Client.UnfollowNewsletter(ctx, chatJID)) } else if chatJID.Server == types.GroupServer { memberInfo, err := wa.Main.Bridge.Matrix.GetMemberInfo(ctx, msg.Portal.MXID, wa.UserLogin.UserMXID) if err != nil { @@ -712,7 +712,7 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg err = wa.Client.LeaveGroup(ctx, chatJID) if err != nil { // TODO ignore errors saying you already left the group? - return fmt.Errorf("failed to leave group before deleting chat: %w", err) + return wrapWhatsAppError(fmt.Errorf("failed to leave group before deleting chat: %w", err)) } } } @@ -721,7 +721,7 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg return err } defer wa.mcTrack(msg, time.Now(), &retErr) - return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true)) + return wrapWhatsAppError(wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true))) } func (wa *WhatsAppClient) HandleMatrixBlockUser(ctx context.Context, msg *bridgev2.MatrixBlockUser) error { @@ -733,7 +733,7 @@ func (wa *WhatsAppClient) HandleMatrixBlockUser(ctx context.Context, msg *bridge return err } if msg.Content.ReportSpam { - return fmt.Errorf("spam reporting is not supported") + return bridgev2.WrapErrorInStatus(errors.New("reporting spam is not supported on WhatsApp")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) } action := events.BlocklistChangeActionUnblock if msg.Content.Block { @@ -753,5 +753,5 @@ func (wa *WhatsAppClient) HandleMatrixBlockUser(ctx context.Context, msg *bridge } }() } - return err + return wrapWhatsAppError(err) } diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 74606ab..11b6f08 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -259,6 +259,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { wa.UserLogin.BridgeState.Send(status.BridgeState{ StateEvent: status.StateBadCredentials, Error: WATemporaryBan, + UserAction: status.UserActionRestart, Message: evt.String(), }) wa.notifyOfflineSyncWaiter(fmt.Errorf("temporary ban: %s", evt.String())) diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 325bc55..387d780 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -171,7 +171,7 @@ func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { err := wl.Client.Connect() if err != nil { wl.Log.Err(err).Msg("Failed to connect to WhatsApp for QR login") - return nil, err + return nil, wrapWhatsAppRespError(err) } ctx, cancel := context.WithTimeout(ctx, LoginConnectWait) defer cancel() @@ -202,7 +202,7 @@ func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) err := wl.Client.Connect() if err != nil { wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login") - return nil, err + return nil, wrapWhatsAppRespError(err) } connectCtx, cancelConnect := context.WithTimeout(ctx, LoginConnectWait) err = wl.WaitForQRs.Wait(connectCtx) @@ -223,7 +223,7 @@ func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) } else if errors.Is(err, whatsmeow.ErrIQRateOverLimit) { return nil, ErrRateLimitedByWhatsApp } - return nil, err + return nil, wrapWhatsAppRespError(err) } wl.Log.Debug().Msg("Phone code login started") return &bridgev2.LoginStep{ @@ -461,7 +461,7 @@ func (wl *WALogin) SubmitWebAuthnResponse(ctx context.Context, rawResp json.RawM if err != nil { wl.Log.Err(err).Msg("Failed to send WebAuthn response") wl.Cancel() - return nil, err + return nil, wrapWhatsAppRespError(err) } select { case <-ctx.Done(): @@ -480,7 +480,7 @@ func (wl *WALogin) SubmitWebAuthnResponse(ctx context.Context, rawResp json.RawM if err != nil { wl.Log.Err(err).Msg("Failed to send WebAuthn confirmation") wl.Cancel() - return nil, err + return nil, wrapWhatsAppRespError(err) } case <-wl.LoginComplete.GetChan(): } diff --git a/pkg/connector/proxy.go b/pkg/connector/proxy.go index 5ae103d..fe1d7ff 100644 --- a/pkg/connector/proxy.go +++ b/pkg/connector/proxy.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "time" "github.com/rs/zerolog" "go.mau.fi/whatsmeow" @@ -14,11 +15,13 @@ import ( // TODO move proxy stuff to mautrix-go +var getProxyClient = &http.Client{Timeout: 30 * time.Second} + type respGetProxy struct { ProxyURL string `json:"proxy_url"` } -func (wa *WhatsAppConnector) getProxy(reason string) (string, error) { +func (wa *WhatsAppConnector) getProxy(ctx context.Context, reason string) (string, error) { if wa.Config.GetProxyURL == "" { return wa.Config.Proxy, nil } @@ -29,15 +32,17 @@ func (wa *WhatsAppConnector) getProxy(reason string) (string, error) { q := parsed.Query() q.Set("reason", reason) parsed.RawQuery = q.Encode() - req, err := http.NewRequest(http.MethodGet, parsed.String(), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil) if err != nil { return "", fmt.Errorf("failed to prepare request: %w", err) } req.Header.Set("User-Agent", mautrix.DefaultUserAgent) - resp, err := http.DefaultClient.Do(req) + resp, err := getProxyClient.Do(req) if err != nil { return "", fmt.Errorf("failed to send request: %w", err) - } else if resp.StatusCode >= 300 || resp.StatusCode < 200 { + } + defer resp.Body.Close() + if resp.StatusCode >= 300 || resp.StatusCode < 200 { return "", fmt.Errorf("unexpected status code %d", resp.StatusCode) } var respData respGetProxy @@ -56,7 +61,7 @@ func (wa *WhatsAppConnector) updateProxy(ctx context.Context, client *whatsmeow. if isLogin { reason = "login" } - if proxy, err := wa.getProxy(reason); err != nil { + if proxy, err := wa.getProxy(ctx, reason); err != nil { return fmt.Errorf("failed to get proxy address: %w", err) } else if proxy == "" { return nil diff --git a/pkg/connector/startchat.go b/pkg/connector/startchat.go index 18e8326..d4e5f8e 100644 --- a/pkg/connector/startchat.go +++ b/pkg/connector/startchat.go @@ -88,7 +88,7 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) } else if entry, ok := isOnWhatsappCache.Get(number); ok && time.Since(entry.ts) < 4*time.Hour { return entry.jid, nil } else if resp, err := wa.Client.IsOnWhatsApp(ctx, []string{number}); err != nil { - return types.EmptyJID, fmt.Errorf("failed to check if number is on WhatsApp: %w", err) + return types.EmptyJID, wrapWhatsAppRespError(fmt.Errorf("failed to check if number is on WhatsApp: %w", err)) } else if len(resp) == 0 { return types.EmptyJID, fmt.Errorf("the server did not respond to the query") } else if !resp[0].IsIn { @@ -128,7 +128,7 @@ func (wa *WhatsAppClient) startChatPNToLID(ctx context.Context, jid types.JID) ( } else if lid.IsEmpty() { resp, err := wa.Client.GetUserInfo(ctx, []types.JID{jid}) if err != nil { - return jid, fmt.Errorf("failed to get user info for phone number: %w", err) + return jid, wrapWhatsAppRespError(fmt.Errorf("failed to get user info for phone number: %w", err)) } else if info, ok := resp[jid]; !ok { return jid, fmt.Errorf("server didn't return user info for phone number") } else if info.LID.IsEmpty() { @@ -292,7 +292,7 @@ func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.Grou } resp, err := wa.Client.CreateGroup(ctx, req) if err != nil { - return nil, fmt.Errorf("failed to create group: %w", err) + return nil, wrapWhatsAppRespError(fmt.Errorf("failed to create group: %w", err)) } failedParticipants := make(map[networkid.UserID]*bridgev2.CreateChatFailedParticipant) filteredParticipants := resp.Participants[:0] diff --git a/pkg/msgconv/matrixpoll.go b/pkg/msgconv/matrixpoll.go index fab8330..0fe0026 100644 --- a/pkg/msgconv/matrixpoll.go +++ b/pkg/msgconv/matrixpoll.go @@ -57,8 +57,8 @@ func (mc *MessageConverter) msc1767ToWhatsApp(ctx context.Context, msg event.MSC } var ( - errPollMissingQuestion = bridgev2.WrapErrorInStatus(errors.New("poll message is missing question")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported) - errPollDuplicateOption = bridgev2.WrapErrorInStatus(errors.New("poll options must be unique")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported) + errPollMissingQuestion = bridgev2.WrapErrorInStatus(errors.New("poll message is missing question")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) + errPollDuplicateOption = bridgev2.WrapErrorInStatus(errors.New("poll options must be unique")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) ) func (mc *MessageConverter) PollStartToWhatsApp( From c8564186f4b81ee8a80edbaa105d10d7ad27d9c0 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Thu, 22 Oct 2026 15:09:03 +0000 Subject: [PATCH 096/139] msgconv/from-whatsapp: cut off long messages after adding mentions (#988) --- pkg/msgconv/from-whatsapp.go | 1 + pkg/msgconv/wa-text.go | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go index 99d036f..c537e76 100644 --- a/pkg/msgconv/from-whatsapp.go +++ b/pkg/msgconv/from-whatsapp.go @@ -320,6 +320,7 @@ func (mc *MessageConverter) ToMatrix( } cm.ThreadRoot = ptr.Ptr(waid.MakeMessageID(chat, pcp, commentTarget.GetID())) } + mc.cutOffLongMessage(part.Content) return cm } diff --git a/pkg/msgconv/wa-text.go b/pkg/msgconv/wa-text.go index 7859f37..1a01633 100644 --- a/pkg/msgconv/wa-text.go +++ b/pkg/msgconv/wa-text.go @@ -21,6 +21,7 @@ import ( "fmt" "html" "regexp" + "slices" "strconv" "strings" @@ -42,16 +43,29 @@ func (mc *MessageConverter) convertTextMessage(ctx context.Context, msg *waE2E.M } contextInfo = msg.GetExtendedTextMessage().GetContextInfo() mc.parseFormatting(part.Content, false, false) - if len(part.Content.FormattedBody) > 20000 { - part.Content.Body = part.Content.Body[:500] + "[...]" - } - if len(part.Content.FormattedBody) > 40000 { - part.Content.FormattedBody = part.Content.FormattedBody[:40000] + "
[long message cut off]" - } part.Content.BeeperLinkPreviews = mc.convertURLPreviewToBeeper(ctx, msg.GetExtendedTextMessage()) return } +func (mc *MessageConverter) cutOffLongMessage(content *event.MessageEventContent) { + if len(content.FormattedBody) <= 20000 && len(content.Body) <= 20000 { + return + } + if content.FormattedBody == "" { + if len(content.Body) > 40000 { + content.Body = content.Body[:40000] + "\n\n[long message cut off]" + } + } else if len(content.Body) > 500 { + content.Body = content.Body[:500] + "[...]" + } + if len(content.FormattedBody) > 40000 { + content.FormattedBody = content.FormattedBody[:40000] + "
[long message cut off]" + } + if content.Mentions != nil { + content.Mentions.UserIDs = slices.DeleteFunc(content.Mentions.UserIDs, mc.Bridge.IsGhostMXID) + } +} + func (mc *MessageConverter) parseFormatting(content *event.MessageEventContent, allowInlineURL, forceHTML bool) { parsedHTML := parseWAFormattingToHTML(content.Body, allowInlineURL) if forceHTML || parsedHTML != event.TextToHTML(content.Body) { From 33b7768ef0d0c781e6e2785f083b9d81adb12822 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Thu, 22 Oct 2026 15:10:37 +0000 Subject: [PATCH 097/139] handlewhatsapp: ignore chat deletes older than the last message (#984) --- pkg/connector/handlewhatsapp.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 11b6f08..e7a9b65 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -714,6 +714,27 @@ func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.De Type: bridgev2.RemoteEventChatDelete, PortalKey: wa.makeWAPortalKey(chatJID), Timestamp: evt.Timestamp, + PreHandleWithResultFunc: func(ctx context.Context, portal *bridgev2.Portal) *bridgev2.EventHandlingResult { + messageRange := evt.Action.GetMessageRange() + deletedAt := max( + evt.Timestamp.Unix(), + messageRange.GetLastMessageTimestamp(), + messageRange.GetLastSystemMessageTimestamp(), + ) + if deletedAt <= 0 { + return nil + } + lastMessage, err := wa.Main.Bridge.DB.Message.GetLastNInPortal(ctx, portal.PortalKey, 1) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to get last message to check if chat delete is outdated") + } else if len(lastMessage) == 1 && lastMessage[0].Timestamp.Unix() > deletedAt { + zerolog.Ctx(ctx).Debug(). + Time("last_message_ts", lastMessage[0].Timestamp). + Msg("Ignoring chat delete that is older than the last message") + return ptr.Ptr(bridgev2.EventHandlingResultIgnored) + } + return nil + }, }, OnlyForMe: true, Children: true, From 83f6cca39bf8676ee805ba02eabdee7bcf5b7d45 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Thu, 22 Oct 2026 18:39:55 +0000 Subject: [PATCH 098/139] msgconv/from-whatsapp: reply to the pinned message in pin notices (#989) --- pkg/msgconv/from-whatsapp.go | 2 +- pkg/msgconv/wa-misc.go | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go index c537e76..1ff2e02 100644 --- a/pkg/msgconv/from-whatsapp.go +++ b/pkg/msgconv/from-whatsapp.go @@ -187,7 +187,7 @@ func (mc *MessageConverter) ToMatrix( case waMsg.EventMessage != nil: part, contextInfo = mc.convertEventMessage(ctx, waMsg.EventMessage) case waMsg.PinInChatMessage != nil: - part, contextInfo = mc.convertPinInChatMessage(ctx, waMsg.PinInChatMessage) + part, contextInfo = mc.convertPinInChatMessage(ctx, info, waMsg.PinInChatMessage) case waMsg.KeepInChatMessage != nil: part, contextInfo = mc.convertKeepInChatMessage(ctx, waMsg.KeepInChatMessage) case waMsg.RichResponseMessage != nil: diff --git a/pkg/msgconv/wa-misc.go b/pkg/msgconv/wa-misc.go index 275b132..a0d7097 100644 --- a/pkg/msgconv/wa-misc.go +++ b/pkg/msgconv/wa-misc.go @@ -322,11 +322,19 @@ func (mc *MessageConverter) convertEventMessage(ctx context.Context, msg *waE2E. }, msg.GetContextInfo() } -func (mc *MessageConverter) convertPinInChatMessage(ctx context.Context, msg *waE2E.PinInChatMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { +func (mc *MessageConverter) convertPinInChatMessage(ctx context.Context, info *types.MessageInfo, msg *waE2E.PinInChatMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { body := "Pinned a message" if msg.GetType() == waE2E.PinInChatMessage_UNPIN_FOR_ALL { body = "Unpinned a message" } + var contextInfo *waE2E.ContextInfo + if target, err := waid.ParseMessageID(KeyToMessageID(ctx, getClient(ctx), info.Chat, info.Sender, msg.GetKey())); err == nil { + contextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(target.ID), + Participant: proto.String(target.Sender.String()), + RemoteJID: proto.String(target.Chat.String()), + } + } return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, @@ -334,7 +342,7 @@ func (mc *MessageConverter) convertPinInChatMessage(ctx context.Context, msg *wa MsgType: event.MsgNotice, Body: body, }, - }, nil + }, contextInfo } func (mc *MessageConverter) convertKeepInChatMessage(ctx context.Context, msg *waE2E.KeepInChatMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { From 8f1937b25941872fa1abbff73970ef35e0b8870e Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 26 Oct 2026 06:18:12 -0400 Subject: [PATCH 099/139] msgconv/rich-response: don't include raw data if the message would be too large (#996) --- pkg/msgconv/wa-rich-response.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/msgconv/wa-rich-response.go b/pkg/msgconv/wa-rich-response.go index 41ac9ab..28a648c 100644 --- a/pkg/msgconv/wa-rich-response.go +++ b/pkg/msgconv/wa-rich-response.go @@ -66,10 +66,15 @@ func (mc *MessageConverter) convertUnifiedRichResponseMessage(ctx context.Contex Extra: extra, } } + content := format.HTMLToContent(htmlBuf.String()) + if len(data)+len(content.Body)+len(content.FormattedBody) > 40000 { + delete(extra, "fi.mau.whatsapp.rich_message") + delete(extra, "fi.mau.whatsapp.non_json_rich_message") + } extra["fi.mau.whatsapp.rich_response_id"] = rrMsg.ResponseID return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, - Content: new(format.HTMLToContent(htmlBuf.String())), + Content: &content, Extra: extra, } } From 56a19a1d10b3ed17b0409174a9327bde9fefd96b Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 26 Oct 2026 06:18:46 -0400 Subject: [PATCH 100/139] msgconv/from-whatsapp: count mentions when cutting off long messages (#995) --- pkg/msgconv/wa-text.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/msgconv/wa-text.go b/pkg/msgconv/wa-text.go index 1a01633..52874fd 100644 --- a/pkg/msgconv/wa-text.go +++ b/pkg/msgconv/wa-text.go @@ -48,22 +48,28 @@ func (mc *MessageConverter) convertTextMessage(ctx context.Context, msg *waE2E.M } func (mc *MessageConverter) cutOffLongMessage(content *event.MessageEventContent) { - if len(content.FormattedBody) <= 20000 && len(content.Body) <= 20000 { + size := len(content.Body) + len(content.FormattedBody) + if content.Mentions != nil { + for _, userID := range content.Mentions.UserIDs { + size += len(userID) + } + } + if size <= 40000 { return } + if content.Mentions != nil { + content.Mentions.UserIDs = slices.DeleteFunc(content.Mentions.UserIDs, mc.Bridge.IsGhostMXID) + } if content.FormattedBody == "" { if len(content.Body) > 40000 { content.Body = content.Body[:40000] + "\n\n[long message cut off]" } - } else if len(content.Body) > 500 { + } else if len(content.FormattedBody) > 20000 && len(content.Body) > 500 { content.Body = content.Body[:500] + "[...]" } if len(content.FormattedBody) > 40000 { content.FormattedBody = content.FormattedBody[:40000] + "
[long message cut off]" } - if content.Mentions != nil { - content.Mentions.UserIDs = slices.DeleteFunc(content.Mentions.UserIDs, mc.Bridge.IsGhostMXID) - } } func (mc *MessageConverter) parseFormatting(content *event.MessageEventContent, allowInlineURL, forceHTML bool) { From 1678681af7a849b66ababf66a928d874db5186da Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 26 Oct 2026 06:24:17 -0400 Subject: [PATCH 101/139] chatinfo: don't create rooms for newsletters that no longer exist (#993) --- pkg/connector/backfill.go | 2 +- pkg/connector/chatinfo.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index f1fc56e..629039d 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -435,7 +435,7 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) { return } wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true) - if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) { + if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) || errors.Is(err, ErrNewsletterNotFound) { log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID). Msg("Skipping creating room because the user is not a participant") //err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID) diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 61b9e74..ce8c739 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -31,6 +31,7 @@ func (wa *WhatsAppClient) GetChatInfo(ctx context.Context, portal *bridgev2.Port } var ErrBroadcastList = errors.New("broadcast list bridging is currently not supported") +var ErrNewsletterNotFound = errors.New("newsletter doesn't exist") func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation, isNew bool) (wrapped *bridgev2.ChatInfo, err error) { switch portalJID.Server { @@ -56,6 +57,8 @@ func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, info, err := wa.Client.GetNewsletterInfo(ctx, portalJID) if err != nil { return nil, err + } else if info == nil || info.ID.IsEmpty() { + return nil, ErrNewsletterNotFound } wrapped = wa.wrapNewsletterInfo(ctx, info) default: From 2759bc5ea25237fee4645e9e2c131f014362cfb7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 26 Oct 2026 14:17:07 +0200 Subject: [PATCH 102/139] msgconv/from-whatsapp: add support for encrypted direct media --- go.mod | 2 +- go.sum | 4 ++-- pkg/msgconv/wa-media.go | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 0c70c3d..4dc9322 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( golang.org/x/sync v0.24.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.32.0 + maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae ) require ( diff --git a/go.sum b/go.sum index 576ec24..b698912 100644 --- a/go.sum +++ b/go.sum @@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.32.0 h1:xuAZzzyWce0pFkBhV/MCLFiJdVs2rFkY3Eep7hNng4I= -maunium.net/go/mautrix v0.32.0/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= +maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae h1:4SY5+nh2B968L5NmMGmF0WPRbV9D1yFLP8U+JASuHXg= +maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go index 20a1a14..ff7008b 100644 --- a/pkg/msgconv/wa-media.go +++ b/pkg/msgconv/wa-media.go @@ -91,7 +91,9 @@ func (mc *MessageConverter) convertMediaMessage( 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, preparedMedia.File, err = portal.Bridge.GenerateContentURI( + ctx, portal.MXID, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver, getMediaIDVersion(msg)), + ) if err != nil { panic(fmt.Errorf("failed to generate content URI: %w", err)) } From 960b4ed000a6d8da7b6a1d3094bc73389b637830 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 26 Oct 2026 14:54:30 +0200 Subject: [PATCH 103/139] client: send unified session on connect Closes #992 --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/client.go | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4dc9322..c58cd7c 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/yuin/goldmark v1.8.6 go.mau.fi/util v0.11.0 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20261016130820-0b0a1cdac3e2 + go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a golang.org/x/image v0.47.0 golang.org/x/net v0.61.0 golang.org/x/sync v0.24.0 diff --git a/go.sum b/go.sum index b698912..df5df11 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ go.mau.fi/util v0.11.0 h1:JNEpkVRxfTdJUC0DIGKiRlLKzl4G/qLX/eVzoP7QuwY= go.mau.fi/util v0.11.0/go.mod h1:04AEFVsc4zNdIkDC1gkdqbg/nZFolHY50pyXh2DSy7s= go.mau.fi/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-20261016130820-0b0a1cdac3e2 h1:+EhsWM5VeE5Vu3KRlk3PmYAY0lEYkbhI5W4mTmM9X/Q= -go.mau.fi/whatsmeow v0.0.0-20261016130820-0b0a1cdac3e2/go.mod h1:iY4lm61nTLWsNiIE1EmHUZwhKGBeoV4dhnVE2uMtNoQ= +go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a h1:ZyCV/mWfRkutxs5R0K3v+oyTbF4huprSlPZg/LEFQ7k= +go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a/go.mod h1:iY4lm61nTLWsNiIE1EmHUZwhKGBeoV4dhnVE2uMtNoQ= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= diff --git a/pkg/connector/client.go b/pkg/connector/client.go index cd52be2..4181235 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -84,6 +84,7 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. w.Client.ManualHistorySyncDownload = true w.Client.SendReportingTokens = true w.Client.AutomaticMessageRerequestFromPhone = true + w.Client.UnifiedSessionInterval = 24 * time.Hour w.Client.GetMessageForRetry = w.trackNotFoundRetry w.Client.PreRetryCallback = w.trackFoundRetry w.Client.BackgroundEventCtx = w.UserLogin.Log.WithContext(wa.Bridge.BackgroundCtx) From 322485c5a983682b0023bb94281a9ce08a29be8a Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 26 Oct 2026 12:14:24 -0400 Subject: [PATCH 104/139] handlewhatsapp: match own phone number in bot chat receipts (#1000) --- pkg/connector/handlewhatsapp.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index e7a9b65..4ba4f10 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -514,6 +514,10 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei } else if evt.Chat.Server == types.NewsletterServer { messageSender = evt.Chat } + var messageSenderAlt types.JID + if evt.MessageSender.IsEmpty() && evt.Chat.Server == types.BotServer { + messageSenderAlt = wa.JID + } var chatAlt types.JID if evt.Chat.Server == types.DefaultUserServer { chatLID, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Chat) @@ -527,6 +531,9 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei if !chatAlt.IsEmpty() { targets = append(targets, waid.MakeMessageID(chatAlt, messageSender, id)) } + if !messageSenderAlt.IsEmpty() { + targets = append(targets, waid.MakeMessageID(evt.Chat, messageSenderAlt, id)) + } } senderLID := evt.Sender if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() { From 57e055c3adb94c63849ac35ecc89a5f32ef3de74 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 26 Oct 2026 16:42:19 +0000 Subject: [PATCH 105/139] lidmigrate: move Meta AI phone number portals to the bot JID (#998) --- pkg/connector/backfill.go | 8 ++++++-- pkg/connector/connector.go | 4 ++++ pkg/connector/lidmigrate.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 629039d..0e319e2 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -249,7 +249,9 @@ func (wa *WhatsAppClient) handleWAHistorySync( } else { totalMessageCount += len(conv.GetMessages()) } - if jid.Server == types.DefaultUserServer { + if jid == types.MetaAIJID { + jid = types.NewMetaAIJID + } else if jid.Server == types.DefaultUserServer { lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) if err != nil { log.Err(err).Stringer("pn", jid).Msg("Failed to get LID for phone number in history sync") @@ -803,7 +805,9 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID") continue } - if portalJID.Server == types.DefaultUserServer { + if portalJID == types.MetaAIJID { + portalJID = types.NewMetaAIJID + } else 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") diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index d7261d3..fb3cdc8 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -164,6 +164,10 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to migrate to LID DMs: %w", err) } + err = wa.migrateMetaAIPortals(ctx) + if err != nil { + return fmt.Errorf("failed to migrate Meta AI portals: %w", err) + } go func() { err = wa.syncMismatchingGhosts(wa.Bridge.BackgroundCtx) if err != nil { diff --git a/pkg/connector/lidmigrate.go b/pkg/connector/lidmigrate.go index ec1c700..befef07 100644 --- a/pkg/connector/lidmigrate.go +++ b/pkg/connector/lidmigrate.go @@ -47,6 +47,8 @@ func (wa *WhatsAppClient) FindAltTargetMessage(ctx context.Context, targetMsg ne if err != nil { return nil, err } + } else if parsed.Chat == types.NewMetaAIJID { + altChat = types.MetaAIJID } if !altSender.IsEmpty() { altSenderID := *parsed @@ -58,6 +60,8 @@ func (wa *WhatsAppClient) FindAltTargetMessage(ctx context.Context, targetMsg ne altChatID.Chat = altChat if altSender.Server == types.DefaultUserServer { altChatID.Sender = altSender + } else if parsed.Sender == types.NewMetaAIJID { + altChatID.Sender = types.MetaAIJID } alts = append(alts, altChatID.String()) } @@ -211,6 +215,30 @@ func (wa *WhatsAppConnector) migrateToLIDDMs(ctx context.Context) error { return nil } +func (wa *WhatsAppConnector) migrateMetaAIPortals(ctx context.Context) error { + if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_meta_ai_portals_migrated") == "true" { + return nil + } + log := zerolog.Ctx(ctx).With().Str("action", "migrate meta ai portals").Logger() + const findMetaAIPortals = "SELECT id, receiver FROM portal WHERE bridge_id=$1 AND id=$2" + portalKeys, err := scanPortalKey.NewRowIter(wa.Bridge.DB.Query(ctx, findMetaAIPortals, wa.Bridge.ID, waid.MakePortalID(types.MetaAIJID))).AsList() + if err != nil { + return fmt.Errorf("failed to get Meta AI portals: %w", err) + } + for _, key := range portalKeys { + res, err := wa.reIDPhoneDMToLID(ctx, types.MetaAIJID, types.NewMetaAIJID, key.Receiver) + if err != nil { + return fmt.Errorf("failed to re-ID Meta AI portal for %s: %w", key.Receiver, err) + } + log.Info(). + Str("receiver", string(key.Receiver)). + Stringer("result", res). + Msg("Re-ID'd Meta AI phone number portal") + } + wa.Bridge.DB.KV.Set(ctx, "whatsapp_meta_ai_portals_migrated", "true") + return nil +} + func (wa *WhatsAppConnector) syncMismatchingGhosts(ctx context.Context) error { if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_avatars_resynced") == "true" { return nil From 870414b015739dec7087ea4660b33e11b7f0a237 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 26 Oct 2026 17:58:59 +0000 Subject: [PATCH 106/139] commands: add reset-appstate command (#994) --- pkg/connector/commands.go | 52 +++++++++++++++++++++++++++++++++----- pkg/connector/connector.go | 2 +- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/pkg/connector/commands.go b/pkg/connector/commands.go index 037d24e..120d05e 100644 --- a/pkg/connector/commands.go +++ b/pkg/connector/commands.go @@ -148,13 +148,7 @@ func fnSync(ce *commands.Event) { case "appstate": names := appstate.AllPatchNames[:] if len(ce.Args) > 1 { - names = exslices.CastFuncFilter(ce.Args[1:], func(name string) (appstate.WAPatchName, bool) { - if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) { - ce.Reply("Invalid app state name `%s`", name) - return "", false - } - return appstate.WAPatchName(name), true - }) + names = parseAppStateNames(ce, ce.Args[1:]) } for _, name := range names { err := wa.Client.FetchAppState(ce.Ctx, name, true, false) @@ -174,6 +168,50 @@ func fnSync(ce *commands.Event) { } } +func parseAppStateNames(ce *commands.Event, args []string) []appstate.WAPatchName { + return exslices.CastFuncFilter(args, func(name string) (appstate.WAPatchName, bool) { + if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) { + ce.Reply("Invalid app state name `%s`", name) + return "", false + } + return appstate.WAPatchName(name), true + }) +} + +var cmdResetAppState = &commands.FullHandler{ + Func: fnResetAppState, + Name: "reset-appstate", + Help: commands.HelpMeta{ + Section: commands.HelpSectionAdmin, + Description: "Ask your phone to reset broken app state. This logs out all linked devices.", + Args: "", + }, + RequiresLogin: true, +} + +func fnResetAppState(ce *commands.Event) { + login := ce.User.GetDefaultLogin() + if login == nil { + ce.Reply("Login not found") + return + } + if len(ce.Args) == 0 { + ce.Reply("Usage: `$cmdprefix reset-appstate `\n\nThis logs out all linked devices, including the bridge. You'll have to log in again afterwards.") + return + } + names := parseAppStateNames(ce, ce.Args) + if len(names) != len(ce.Args) { + return + } + wa := login.Client.(*WhatsAppClient) + _, err := wa.Client.SendPeerMessage(ce.Ctx, whatsmeow.BuildFatalAppStateExceptionNotification(names...)) + if err != nil { + ce.Reply("Failed to send app state reset request: %v", err) + return + } + ce.Reply("Sent app state reset request. Your phone should log out all linked devices shortly, after which you can log in again.") +} + var cmdInviteLink = &commands.FullHandler{ Func: fnInviteLink, Name: "invite-link", diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index fb3cdc8..94c66eb 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -110,7 +110,7 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) { wa.DB = wadb.New(bridge.ID, bridge.DB.Database, bridge.Log.With().Str("db_section", "whatsapp").Logger()) wa.MsgConv.DB = wa.DB wa.Bridge.Commands.(*commands.Processor).AddHandlers( - cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin, + cmdAccept, cmdSync, cmdResetAppState, cmdInviteLink, cmdResolveLink, cmdJoin, ) wa.mediaEditCache = make(MediaEditCache) wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]() From bb09b7584cc7ac28a4a2fdcd17b71fa2af2f6d36 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 27 Oct 2026 10:42:29 +0000 Subject: [PATCH 107/139] msgconv/from-matrix: convert non-h264 videos when sending (#987) --- pkg/msgconv/from-matrix.go | 58 +++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index 192b8a6..ba2adc2 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -27,12 +27,15 @@ import ( "image/color" "image/jpeg" "net/http" + "os" + "path/filepath" "slices" "strconv" "strings" "time" "github.com/rs/zerolog" + "go.mau.fi/util/exmime" "go.mau.fi/util/ffmpeg" "go.mau.fi/util/ptr" "go.mau.fi/util/random" @@ -515,6 +518,56 @@ func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) { return webpBuffer.Bytes(), size, nil } +func convertUnsupportedVideoCodec(ctx context.Context, data []byte, mime string) ([]byte, string, error) { + if !ffmpeg.Supported() || !ffmpeg.ProbeSupported() { + return data, mime, nil + } + log := zerolog.Ctx(ctx) + tempdir, err := os.MkdirTemp("", "mautrix_whatsapp_video_*") + if err != nil { + log.Warn().Err(err).Msg("Failed to create temp dir for video, sending it as-is") + return data, mime, nil + } + defer os.RemoveAll(tempdir) + inputPath := filepath.Join(tempdir, "input"+exmime.ExtensionFromMimetype(mime)) + err = os.WriteFile(inputPath, data, 0600) + if err != nil { + log.Warn().Err(err).Msg("Failed to write video to temp file, sending it as-is") + return data, mime, nil + } + probe, err := ffmpeg.Probe(ctx, inputPath) + if err != nil { + log.Warn().Err(err).Msg("Failed to probe video, sending it as-is") + return data, mime, nil + } + videoSupported := !slices.ContainsFunc(probe.Streams, func(stream *ffmpeg.Stream) bool { + return stream.CodecType == "video" && stream.Disposition.AttachedPic == 0 && stream.CodecName != "h264" + }) + audioSupported := !slices.ContainsFunc(probe.Streams, func(stream *ffmpeg.Stream) bool { + return stream.CodecType == "audio" && stream.CodecName != "aac" + }) + if videoSupported && audioSupported { + return data, mime, nil + } + videoArgs := []string{"-c:v", "copy"} + if !videoSupported { + videoArgs = []string{ + "-pix_fmt", "yuv420p", "-c:v", "h264", "-b:v", "2M", + "-filter:v", "crop='floor(in_w/2)*2:floor(in_h/2)*2'", + } + } + outputPath := filepath.Join(tempdir, "output.mp4") + err = ffmpeg.ConvertPathWithDestination(ctx, inputPath, outputPath, nil, append(videoArgs, "-c:a", "aac", "-movflags", "+faststart"), false) + if err != nil { + return nil, mime, err + } + converted, err := os.ReadFile(outputPath) + if err != nil { + return nil, mime, err + } + return converted, "video/mp4", nil +} + func (mc *MessageConverter) getOriginalBridgedSticker(ctx context.Context, info *event.BridgedSticker) (*types.StickerPackItem, error) { if info == nil || info.Network != StickerSourceID || !strings.HasPrefix(info.PackURL, StickerPackURLPrefix) || info.ID == "" { return nil, nil @@ -609,7 +662,10 @@ func (mc *MessageConverter) reuploadFileToWhatsApp( case event.MsgVideo: switch mime { case "video/mp4", "video/3gpp": - // allowed + data, mime, err = convertUnsupportedVideoCodec(ctx, data, mime) + if err != nil { + return nil, nil, mime, fmt.Errorf("%w (%s to h264): %w", bridgev2.ErrMediaConvertFailed, mime, err) + } case "video/webm", "video/quicktime": sourceFormat := "webm" if mime == "video/quicktime" { From 76c59bd05db9c53dfd4d9181a73c605f52091976 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 27 Oct 2026 10:42:50 +0000 Subject: [PATCH 108/139] backfill: mark history sync messages read up to the unread count (#1001) --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/backfill.go | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c58cd7c..fc19065 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( golang.org/x/sync v0.24.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae + maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d ) require ( diff --git a/go.sum b/go.sum index df5df11..2775f5f 100644 --- a/go.sum +++ b/go.sum @@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae h1:4SY5+nh2B968L5NmMGmF0WPRbV9D1yFLP8U+JASuHXg= -maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= +maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d h1:DsMn0i7cBKcQZ0AVxJm9hfWAu6xJdjRrf457EtXxyNY= +maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 0e319e2..54edbaa 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -595,9 +595,28 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet resp.HasMore = hasMore resp.Forward = params.Forward resp.MarkRead = markRead + if params.Forward && !markRead && conv != nil && !ptr.Val(conv.MarkedAsUnread) { + resp.ReadUpTo = historySyncReadUpTo(resp.Messages, int(ptr.Val(conv.UnreadCount))) + } return resp, nil } +func historySyncReadUpTo(messages []*bridgev2.BackfillMessage, unreadCount int) time.Time { + if unreadCount <= 0 { + return time.Time{} + } + for i := len(messages) - 1; i >= 0; i-- { + if messages[i].Sender.IsFromMe { + continue + } + unreadCount-- + if unreadCount == 0 { + return messages[i].Timestamp.Add(-time.Nanosecond) + } + } + return time.Time{} +} + func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) { var err error var rows int64 From 021504afb01a5f117dd3ffa368f5a3afe8b19f51 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 27 Oct 2026 12:28:53 +0000 Subject: [PATCH 109/139] msgconv/wa-media: fix sticker dimension scaling for small non-square stickers (#991) --- pkg/msgconv/wa-media.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go index ff7008b..30c6b30 100644 --- a/pkg/msgconv/wa-media.go +++ b/pkg/msgconv/wa-media.go @@ -255,10 +255,10 @@ func fixStickerDimensions(info *event.FileInfo) { info.Width = WhatsAppStickerSize info.Height = WhatsAppStickerSize } else if info.Width > info.Height { - info.Height /= info.Width / WhatsAppStickerSize + info.Height = info.Height * WhatsAppStickerSize / info.Width info.Width = WhatsAppStickerSize } else { - info.Width /= info.Height / WhatsAppStickerSize + info.Width = info.Width * WhatsAppStickerSize / info.Height info.Height = WhatsAppStickerSize } } From 51b5c5e5fb764435f278883adf63a6dc25842e0f Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Tue, 27 Oct 2026 16:45:29 +0000 Subject: [PATCH 110/139] msgconv/from-whatsapp: bridge live location start as m.location (#1002) --- pkg/msgconv/wa-location.go | 101 +++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 33 deletions(-) diff --git a/pkg/msgconv/wa-location.go b/pkg/msgconv/wa-location.go index 4a1e043..52f3f13 100644 --- a/pkg/msgconv/wa-location.go +++ b/pkg/msgconv/wa-location.go @@ -30,6 +30,40 @@ import ( "maunium.net/go/mautrix/event" ) +func formatCoordinates(lat, long float64) string { + latChar := 'N' + if lat < 0 { + latChar = 'S' + } + longChar := 'E' + if long < 0 { + longChar = 'W' + } + return fmt.Sprintf("%.4f° %c %.4f° %c", math.Abs(lat), latChar, math.Abs(long), longChar) +} + +func addLocationThumbnail(ctx context.Context, content *event.MessageEventContent, thumbnail []byte) { + if len(thumbnail) == 0 { + return + } + thumbnailMime := http.DetectContentType(thumbnail) + thumbnailURL, thumbnailFile, err := getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, thumbnail, "thumb.jpeg", thumbnailMime) + if err != nil { + return + } + cfg, _, _ := image.DecodeConfig(bytes.NewReader(thumbnail)) + content.Info = &event.FileInfo{ + ThumbnailInfo: &event.FileInfo{ + Size: len(thumbnail), + Width: cfg.Width, + Height: cfg.Height, + MimeType: thumbnailMime, + }, + ThumbnailURL: thumbnailURL, + ThumbnailFile: thumbnailFile, + } +} + func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE2E.LocationMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { url := msg.GetURL() if len(url) == 0 { @@ -37,15 +71,7 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE } name := msg.GetName() if len(name) == 0 { - latChar := 'N' - if msg.GetDegreesLatitude() < 0 { - latChar = 'S' - } - longChar := 'E' - if msg.GetDegreesLongitude() < 0 { - longChar = 'W' - } - name = fmt.Sprintf("%.4f° %c %.4f° %c", math.Abs(msg.GetDegreesLatitude()), latChar, math.Abs(msg.GetDegreesLongitude()), longChar) + name = formatCoordinates(msg.GetDegreesLatitude(), msg.GetDegreesLongitude()) } content := &event.MessageEventContent{ @@ -55,24 +81,7 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE FormattedBody: fmt.Sprintf(`Location: %s
%s`, html.EscapeString(url), html.EscapeString(name), html.EscapeString(msg.GetAddress())), GeoURI: fmt.Sprintf("geo:%.5f,%.5f", msg.GetDegreesLatitude(), msg.GetDegreesLongitude()), } - - if len(msg.GetJPEGThumbnail()) > 0 { - thumbnailMime := http.DetectContentType(msg.GetJPEGThumbnail()) - thumbnailURL, thumbnailFile, err := getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, msg.GetJPEGThumbnail(), "thumb.jpeg", thumbnailMime) - if err == nil { - cfg, _, _ := image.DecodeConfig(bytes.NewReader(msg.GetJPEGThumbnail())) - content.Info = &event.FileInfo{ - ThumbnailInfo: &event.FileInfo{ - Size: len(msg.GetJPEGThumbnail()), - Width: cfg.Width, - Height: cfg.Height, - MimeType: thumbnailMime, - }, - ThumbnailURL: thumbnailURL, - ThumbnailFile: thumbnailFile, - } - } - } + addLocationThumbnail(ctx, content, msg.GetJPEGThumbnail()) return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, @@ -81,14 +90,40 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE } func (mc *MessageConverter) convertLiveLocationMessage(ctx context.Context, msg *waE2E.LiveLocationMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { + if msg.DegreesLatitude == nil || msg.DegreesLongitude == nil { + content := &event.MessageEventContent{ + Body: "Started sharing live location", + MsgType: event.MsgNotice, + } + if len(msg.GetCaption()) > 0 { + content.Body += ": " + msg.GetCaption() + } + content.Body += "\n\nUse the WhatsApp app to see the location." + return &bridgev2.ConvertedMessagePart{ + Type: event.EventMessage, + Content: content, + }, msg.GetContextInfo() + } + + lat, long := msg.GetDegreesLatitude(), msg.GetDegreesLongitude() + url := fmt.Sprintf("https://maps.google.com/?q=%.5f,%.5f", lat, long) + name := formatCoordinates(lat, long) + const note = "Live updates are only visible in the WhatsApp app." + body := fmt.Sprintf("Live location: %s\n%s", name, url) + formattedBody := fmt.Sprintf(`Live location: %s`, html.EscapeString(url), html.EscapeString(name)) + if caption := msg.GetCaption(); len(caption) > 0 { + body += "\n" + caption + formattedBody += "
" + html.EscapeString(caption) + } content := &event.MessageEventContent{ - Body: "Started sharing live location", - MsgType: event.MsgNotice, + MsgType: event.MsgLocation, + Body: body + "\n\n" + note, + Format: event.FormatHTML, + FormattedBody: formattedBody + "

" + note, + GeoURI: fmt.Sprintf("geo:%.5f,%.5f", lat, long), } - if len(msg.GetCaption()) > 0 { - content.Body += ": " + msg.GetCaption() - } - content.Body += "\n\nUse the WhatsApp app to see the location." + addLocationThumbnail(ctx, content, msg.GetJPEGThumbnail()) + return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, Content: content, From 88f5fc738ca4c0abc7ec3792131233a72f56f2a2 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Wed, 28 Oct 2026 20:33:06 +0000 Subject: [PATCH 111/139] lidmigrate: remove phone number ghosts that rejoined LID DMs (#990) --- pkg/connector/connector.go | 4 +++ pkg/connector/lidmigrate.go | 72 +++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 94c66eb..19caace 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -173,6 +173,10 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error { if err != nil { wa.Bridge.Log.Err(err).Msg("Failed to sync mismatching ghosts") } + err = wa.removeRejoinedPNGhosts(wa.Bridge.BackgroundCtx) + if err != nil { + wa.Bridge.Log.Err(err).Msg("Failed to remove rejoined PN ghosts") + } }() return nil } diff --git a/pkg/connector/lidmigrate.go b/pkg/connector/lidmigrate.go index befef07..17b2428 100644 --- a/pkg/connector/lidmigrate.go +++ b/pkg/connector/lidmigrate.go @@ -18,6 +18,7 @@ package connector import ( "context" + "errors" "fmt" "time" @@ -25,9 +26,11 @@ import ( "go.mau.fi/util/dbutil" "go.mau.fi/whatsmeow/types" + "maunium.net/go/mautrix" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "go.mau.fi/mautrix-whatsapp/pkg/waid" ) @@ -295,3 +298,72 @@ func (wa *WhatsAppConnector) syncMismatchingGhosts(ctx context.Context) error { wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_avatars_resynced", "true") return nil } + +func (wa *WhatsAppConnector) removeRejoinedPNGhosts(ctx context.Context) error { + if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dm_pn_ghosts_removed") == "true" { + return nil + } + log := zerolog.Ctx(ctx).With().Str("action", "remove rejoined pn ghosts").Logger() + const findLIDPortals = "SELECT id, mxid FROM portal WHERE bridge_id=$1 AND room_type='dm' AND id LIKE '%@lid' AND mxid IS NOT NULL" + lidPortals, err := scanLIDPortal.NewRowIter(wa.Bridge.DB.Query(ctx, findLIDPortals, wa.Bridge.ID)).AsList() + if err != nil { + return fmt.Errorf("failed to get LID portals: %w", err) + } + var removed, failed int + for _, portal := range lidPortals { + lid, err := waid.ParsePortalID(portal.ID) + if err != nil || lid.Server != types.HiddenUserServer { + continue + } + pn, err := wa.DeviceStore.LIDMap.GetPNForLID(ctx, lid) + if err != nil { + return fmt.Errorf("failed to get PN for LID portal %s: %w", portal.ID, err) + } else if pn.IsEmpty() { + continue + } + pnUserID := waid.MakeUserID(pn) + member, err := wa.Bridge.Matrix.GetMemberInfo(ctx, portal.MXID, wa.Bridge.Matrix.FormatGhostMXID(pnUserID)) + if err != nil { + log.Warn().Err(err).Stringer("room_id", portal.MXID).Msg("Failed to get PN ghost membership") + failed++ + continue + } else if member == nil || member.Membership != event.MembershipJoin { + continue + } + pnGhost, err := wa.Bridge.GetGhostByID(ctx, pnUserID) + if err != nil { + return fmt.Errorf("failed to get PN ghost for %s: %w", pn, err) + } + _, err = pnGhost.Intent.SendState(ctx, portal.MXID, event.StateMember, pnGhost.Intent.GetMXID().String(), &event.Content{ + Parsed: &event.MemberEventContent{Membership: event.MembershipLeave, Reason: "Migrating to LIDs"}, + Raw: map[string]any{"com.beeper.exclude_from_timeline": true}, + }, time.Time{}) + if err != nil { + log.Err(err).Stringer("room_id", portal.MXID).Stringer("pn", pn).Msg("Failed to remove PN ghost from LID DM") + if !errors.Is(err, mautrix.MForbidden) && !errors.Is(err, mautrix.MNotFound) { + failed++ + } + continue + } + removed++ + } + log.Info(). + Int("removed_ghosts", removed). + Int("failed", failed). + Int("total_lid_portals", len(lidPortals)). + Msg("Finished removing rejoined PN ghosts from LID DMs") + if failed == 0 && ctx.Err() == nil { + wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dm_pn_ghosts_removed", "true") + } + return nil +} + +type lidPortal struct { + ID networkid.PortalID + MXID id.RoomID +} + +var scanLIDPortal = dbutil.ConvertRowFn[lidPortal](func(row dbutil.Scannable) (portal lidPortal, err error) { + err = row.Scan(&portal.ID, &portal.MXID) + return +}) From 7a0b4b580875e25169b7afac36d334388424bd4d Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Thu, 29 Oct 2026 10:32:04 +0000 Subject: [PATCH 112/139] login: support client WebSocket tunnels (#1004) --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/login.go | 36 ++++++++++++++++++++++-------------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index fc19065..b059b8b 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( golang.org/x/sync v0.24.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d + maunium.net/go/mautrix v0.32.1-0.20261028161347-0907c3c7611e ) require ( diff --git a/go.sum b/go.sum index 2775f5f..498f286 100644 --- a/go.sum +++ b/go.sum @@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d h1:DsMn0i7cBKcQZ0AVxJm9hfWAu6xJdjRrf457EtXxyNY= -maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= +maunium.net/go/mautrix v0.32.1-0.20261028161347-0907c3c7611e h1:7F0CBiL2wsWCfZ1gZlAc30fVeLKNMzuIQb9PZ2mPwts= +maunium.net/go/mautrix v0.32.1-0.20261028161347-0907c3c7611e/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 387d780..c5d4c0a 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -137,6 +137,7 @@ var ( _ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil) _ bridgev2.LoginProcessUserInput = (*WALogin)(nil) _ bridgev2.LoginProcessWithOverride = (*WALogin)(nil) + _ bridgev2.LoginProcessWithParams = (*WALogin)(nil) _ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil) ) @@ -144,6 +145,14 @@ const LoginConnectWait = 30 * time.Second const LoginPairPhoneWait = 30 * time.Second func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { + return wl.StartWithParams(ctx, bridgev2.LoginStartParams{}) +} + +func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogin) (*bridgev2.LoginStep, error) { + return wl.StartWithParams(ctx, bridgev2.LoginStartParams{Override: old}) +} + +func (wl *WALogin) StartWithParams(ctx context.Context, params bridgev2.LoginStartParams) (*bridgev2.LoginStep, error) { wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect) device := wl.Main.DeviceStore.NewDevice() wl.Client = whatsmeow.NewClient(device, waLog.Zerolog(wl.Log)) @@ -153,8 +162,20 @@ func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { if err := wl.Main.updateProxy(ctx, wl.Client, true); err != nil { return nil, err } + if params.WebSocketHTTP != nil { + wl.Client.SetPreLoginHTTPClient(&http.Client{Transport: params.WebSocketHTTP}) + } if wl.PhoneCode { + if params.Override != nil { + phoneNumber := fmt.Sprintf("+%s", params.Override.ID) + wl.Log.Debug(). + Str("phone_number", phoneNumber). + Msg("Auto-submitting phone number for relogin") + return wl.SubmitUserInput(ctx, map[string]string{ + "phone_number": phoneNumber, + }) + } return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeUserInput, StepID: LoginStepIDPhoneNumber, @@ -184,20 +205,6 @@ func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { return makeQRStep(wl.QRs[0]), nil } -func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogin) (*bridgev2.LoginStep, error) { - step, err := wl.Start(ctx) - if err == nil && step != nil && old != nil && step.StepID == LoginStepIDPhoneNumber { - phoneNumber := fmt.Sprintf("+%s", old.ID) - wl.Log.Debug(). - Str("phone_number", phoneNumber). - Msg("Auto-submitting phone number for relogin") - return wl.SubmitUserInput(ctx, map[string]string{ - "phone_number": phoneNumber, - }) - } - return step, err -} - func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { err := wl.Client.Connect() if err != nil { @@ -316,6 +323,7 @@ func (wl *WALogin) handleEvent(rawEvt any) { wl.LoginError = ErrUnexpectedDisconnect case *events.ManualLoginReconnect: wl.Received515.Set() + return default: wl.Log.Warn().Type("event_type", evt).Msg("Got unexpected event") return From ab1fae7181a25d12bacf40fdb5f77dcbdef002e2 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Thu, 29 Oct 2026 12:57:18 +0000 Subject: [PATCH 113/139] handlematrix: reject empty group names (#1007) --- pkg/connector/handlematrix.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index fec40c3..a37d66f 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -107,6 +107,7 @@ func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2 var ErrBroadcastSendDisabled = bridgev2.WrapErrorInStatus(errors.New("sending status messages is disabled")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) var ErrBroadcastReactionUnsupported = bridgev2.WrapErrorInStatus(errors.New("reacting to status messages is not currently supported")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) var ErrNotCommunityAdmin = bridgev2.WrapErrorInStatus(errors.New("only community admins can send announcements")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusNoPermission).WithStatus(event.MessageStatusFail) +var ErrEmptyGroupName = bridgev2.WrapErrorInStatus(errors.New("WhatsApp group names can't be empty")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage, waMsg *waE2E.Message, req *whatsmeow.SendRequestExtra) (*bridgev2.MatrixMessageResponse, error) { if req == nil { @@ -501,6 +502,8 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev if msg.Portal.RoomType == database.RoomTypeDM { return false, fmt.Errorf("cannot set room name for DM") + } else if msg.Content.Name == "" { + return false, ErrEmptyGroupName } defer wa.mcTrack(msg, time.Now(), &retErr) From 33357ced5ae666d9b8e85a56327f28bfb97a293c Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Thu, 29 Oct 2026 13:15:13 +0000 Subject: [PATCH 114/139] directmedia,handlematrix: report WhatsApp send rejections as permanent failures (#1006) Co-authored-by: Claude Opus 5.5 --- pkg/connector/directmedia.go | 14 +++++++++- pkg/connector/errors.go | 49 +++++++++++++++++++++++++++++++++++ pkg/connector/handlematrix.go | 13 ++++++---- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index b86f7dc..03ef398 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -164,7 +164,19 @@ func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, par return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin) } sticker, err := wa.MsgConv.GetCachedSticker(ctx, waClient.Client, parsedID.Sticker.PackID, parsedID.Sticker.FileHash) - if err != nil { + var httpErr whatsmeow.DownloadHTTPError + if errors.As(err, &httpErr) { + switch httpErr.StatusCode { + case http.StatusNotFound, http.StatusGone: + return nil, mautrix.MNotFound.WithMessage("Sticker pack not found") + case http.StatusUnauthorized, http.StatusForbidden: + return nil, mautrix.MForbidden.WithMessage("WhatsApp denied access to the sticker pack") + case http.StatusTooManyRequests: + return nil, mautrix.MLimitExceeded.WithMessage("Rate limited by WhatsApp while fetching the sticker pack").WithCanRetry(true) + default: + return nil, mautrix.MUnknown.WithMessage("WhatsApp returned HTTP %d while fetching the sticker pack", httpErr.StatusCode).WithStatus(http.StatusBadGateway).WithCanRetry(true) + } + } else if err != nil { return nil, err } else if sticker == nil { return nil, mautrix.MNotFound.WithMessage("Sticker not found in pack") diff --git a/pkg/connector/errors.go b/pkg/connector/errors.go index 1ee1b20..38433d6 100644 --- a/pkg/connector/errors.go +++ b/pkg/connector/errors.go @@ -21,11 +21,16 @@ import ( "errors" "fmt" "net/http" + "strconv" + "strings" + "time" "go.mau.fi/whatsmeow" "maunium.net/go/mautrix" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/event" + + "go.mau.fi/mautrix-whatsapp/pkg/waid" ) // wrapConversionError turns a failure to convert a Matrix event into a message status. Nothing has been @@ -85,6 +90,50 @@ func wrapWhatsAppError(err error) error { } } +// wrapSendError turns an error from sending a message to WhatsApp into a message status. +func (wa *WhatsAppClient) wrapSendError(err error) error { + switch sendErrorCode(err) { + case 403: + return bridgev2.WrapErrorInStatus(err). + WithMessage("you don't have permission to do that on WhatsApp"). + WithErrorReason(event.MessageStatusNoPermission). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + case 463: + msg := "WhatsApp has temporarily restricted your account from messaging new contacts" + if until := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil; until.After(time.Now()) { + msg = fmt.Sprintf("WhatsApp has restricted your account from messaging new contacts until %s", until.UTC().Format("Jan 2, 2006 at 3:04 PM MST")) + } + return bridgev2.WrapErrorInStatus(err). + WithMessage(msg). + WithErrorReason(event.MessageStatusNoPermission). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + case 421, 479: + return bridgev2.WrapErrorInStatus(err). + WithMessage("WhatsApp rejected the message"). + WithErrorReason(event.MessageStatusNetworkError). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + default: + return wrapWhatsAppError(err) + } +} + +// sendErrorCode returns the code WhatsApp rejected a message with, which whatsmeow only includes in the +// error text. +func sendErrorCode(err error) int { + if !errors.Is(err, whatsmeow.ErrServerReturnedError) { + return 0 + } + _, codeStr, _ := strings.Cut(err.Error(), whatsmeow.ErrServerReturnedError.Error()+" ") + code, _ := strconv.Atoi(codeStr) + return code +} + // wrapWhatsAppRespError turns an error from a whatsmeow request made during login or provisioning into // an HTTP error, keeping the original error in the chain. Clients retry 429 and 502-504 automatically, // which isn't wanted for provisioning requests, so those statuses aren't used. diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index a37d66f..d51efcd 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -108,6 +108,7 @@ var ErrBroadcastSendDisabled = bridgev2.WrapErrorInStatus(errors.New("sending st var ErrBroadcastReactionUnsupported = bridgev2.WrapErrorInStatus(errors.New("reacting to status messages is not currently supported")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) var ErrNotCommunityAdmin = bridgev2.WrapErrorInStatus(errors.New("only community admins can send announcements")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusNoPermission).WithStatus(event.MessageStatusFail) var ErrEmptyGroupName = bridgev2.WrapErrorInStatus(errors.New("WhatsApp group names can't be empty")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) +var ErrAddBlockedByPrivacy = bridgev2.WrapErrorInStatus(errors.New("this user's privacy settings don't allow adding them to groups")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusNoPermission).WithStatus(event.MessageStatusFail) func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage, waMsg *waE2E.Message, req *whatsmeow.SendRequestExtra) (*bridgev2.MatrixMessageResponse, error) { if req == nil { @@ -140,7 +141,7 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg if errors.Is(err, whatsmeow.ErrNoPermissionInGroup) { return nil, ErrNotCommunityAdmin } - return nil, wrapWhatsAppError(err) + return nil, wa.wrapSendError(err) } var pickedMessageID networkid.MessageID if resp.Sender == wa.GetLID() { @@ -219,7 +220,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev Metadata: &waid.ReactionMetadata{ SenderDeviceID: wa.JID.Device, }, - }, wrapWhatsAppError(err) + }, wa.wrapSendError(err) } func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) (retErr error) { @@ -249,7 +250,7 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b defer wa.mcTrack(msg, time.Now(), &retErr) resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra) zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response") - return wrapWhatsAppError(err) + return wa.wrapSendError(err) } func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) (retErr error) { @@ -289,7 +290,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M ID: editID, }) log.Trace().Any("response", resp).Msg("WhatsApp edit response") - return wrapWhatsAppError(err) + return wa.wrapSendError(err) } func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) (retErr error) { @@ -314,7 +315,7 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br resp, err := wa.Client.SendMessage(ctx, portalJID, revokeMessage, extra) log.Trace().Any("response", resp).Msg("WhatsApp delete response") - return wrapWhatsAppError(err) + return wa.wrapSendError(err) } func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) { @@ -484,6 +485,8 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg return nil, wrapWhatsAppError(err) } else if len(resp) == 0 { return nil, fmt.Errorf("no response for participant change") + } else if resp[0].Error == 403 && action == whatsmeow.ParticipantChangeAdd { + return nil, ErrAddBlockedByPrivacy } else if resp[0].Error != 0 { return nil, fmt.Errorf("failed to change participant: code %d", resp[0].Error) } From 123ac4f340b248e1b440ae6715a680b519bcc3de Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Thu, 29 Oct 2026 15:35:40 +0000 Subject: [PATCH 115/139] userinfo: don't clear alt ghost avatar when main ghost has no avatar info (#1008) --- pkg/connector/userinfo.go | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index c78b0a8..b109e82 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -30,7 +30,7 @@ var ResyncJitterSeconds = 3600 func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) { lastSync := ghost.Metadata.(*waid.GhostMetadata).LastSync.Time - if lastSync.Add(ResyncMinInterval).After(time.Now()) { + if ghost.AvatarID != "" && lastSync.Add(ResyncMinInterval).After(time.Now()) { return } wa.resyncQueueLock.Lock() @@ -118,12 +118,14 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID var portals []*bridgev2.Portal for jid, item := range queue { var lastSync time.Time + unknownAvatar := false if item.ghost != nil { lastSync = item.ghost.Metadata.(*waid.GhostMetadata).LastSync.Time + unknownAvatar = item.ghost.AvatarID == "" } else if item.portal != nil { lastSync = item.portal.Metadata.(*waid.PortalMetadata).LastSync.Time } - if lastSync.Add(ResyncMinInterval).After(time.Now()) { + if !unknownAvatar && lastSync.Add(ResyncMinInterval).After(time.Now()) { log.Debug(). Stringer("jid", jid). Time("last_sync", lastSync). @@ -165,7 +167,7 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID log.Warn().Stringer("jid", jid).Msg("Didn't get info for puppet in background sync") continue } - userInfo, err := wa.getUserInfo(ctx, jid, info.PictureID, info.PictureID != "" && string(ghost.AvatarID) != info.PictureID) + userInfo, err := wa.getUserInfo(ctx, jid, info.PictureID, ghost.AvatarID == "" || (info.PictureID != "" && string(ghost.AvatarID) != info.PictureID)) if err != nil { log.Err(err).Stringer("jid", jid).Msg("Failed to get user info for puppet in background sync") continue @@ -472,16 +474,19 @@ func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JI } func makeInfoFromGhost(ghost *bridgev2.Ghost) *bridgev2.UserInfo { - return &bridgev2.UserInfo{ - Identifiers: ghost.Identifiers, - Name: &ghost.Name, - Avatar: &bridgev2.Avatar{ - ID: ghost.AvatarID, - Remove: ghost.AvatarID == "" || ghost.AvatarMXC == "", - MXC: ghost.AvatarMXC, - Hash: ghost.AvatarHash, - }, + info := &bridgev2.UserInfo{ + Identifiers: ghost.Identifiers, + Name: &ghost.Name, IsBot: &ghost.IsBot, ExtraProfile: ghost.ExtraProfile, } + if ghost.AvatarID != "" { + info.Avatar = &bridgev2.Avatar{ + ID: ghost.AvatarID, + Remove: ghost.AvatarMXC == "", + MXC: ghost.AvatarMXC, + Hash: ghost.AvatarHash, + } + } + return info } From e8f1d2b3f65d666ccbbd08d515c261f4c2f2e8ed Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Thu, 29 Oct 2026 22:27:40 +0000 Subject: [PATCH 116/139] msgconv/from-whatsapp: use own profile name for self mentions (#1009) --- pkg/msgconv/from-whatsapp.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go index 1ff2e02..db18f35 100644 --- a/pkg/msgconv/from-whatsapp.go +++ b/pkg/msgconv/from-whatsapp.go @@ -17,6 +17,7 @@ package msgconv import ( + "cmp" "context" "fmt" "html" @@ -92,7 +93,7 @@ func (mc *MessageConverter) getBasicUserInfo(ctx context.Context, user types.JID portal := getPortal(ctx) login := mc.Bridge.GetCachedUserLoginByID(waid.MakeUserLoginID(pnJID)) if login != nil && (portal.Receiver == "" || portal.Receiver == login.ID) { - return login.UserMXID, ghost.Name, nil + return login.UserMXID, cmp.Or(login.RemoteProfile.Name, ghost.Name), nil } } return ghost.Intent.GetMXID(), ghost.Name, nil From 1f30d5516cd01424b3348c1f4792c60f7fc12931 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 30 Oct 2026 15:52:27 +0200 Subject: [PATCH 117/139] backfill: drop messages in the whatsapp official updates chat --- pkg/connector/backfill.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 54edbaa..378b58e 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -246,6 +246,11 @@ func (wa *WhatsAppClient) handleWAHistorySync( } else if jid.Server == types.BroadcastServer { log.Debug().Stringer("chat_jid", jid).Msg("Skipping broadcast list in history sync") continue + } else if jid == types.PSAJID { + // New messages in the official updates chat aren't bridged because they use a different path, + // so drop history as well. This should be removed if the PSA chat gets properly bridged in the future. + log.Debug().Stringer("chat_jid", jid).Msg("Skipping PSA in history sync") + continue } else { totalMessageCount += len(conv.GetMessages()) } From 678d4ebb4fa2ae9796789f7276c6fce0f79982b8 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Fri, 30 Oct 2026 16:09:59 +0200 Subject: [PATCH 118/139] wadb: re-arm backfill for chats with leftover history sync --- pkg/connector/wadb/upgrades/00-latest-schema.sql | 2 +- .../11-rearm-leftover-history-sync-backfill.sql | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 pkg/connector/wadb/upgrades/11-rearm-leftover-history-sync-backfill.sql diff --git a/pkg/connector/wadb/upgrades/00-latest-schema.sql b/pkg/connector/wadb/upgrades/00-latest-schema.sql index 0f1fef9..c4172d0 100644 --- a/pkg/connector/wadb/upgrades/00-latest-schema.sql +++ b/pkg/connector/wadb/upgrades/00-latest-schema.sql @@ -1,4 +1,4 @@ --- v0 -> v10 (compatible with v3+): Latest revision +-- v0 -> v11 (compatible with v3+): Latest revision CREATE TABLE whatsapp_poll_option_id ( bridge_id TEXT NOT NULL, diff --git a/pkg/connector/wadb/upgrades/11-rearm-leftover-history-sync-backfill.sql b/pkg/connector/wadb/upgrades/11-rearm-leftover-history-sync-backfill.sql new file mode 100644 index 0000000..69e8b1e --- /dev/null +++ b/pkg/connector/wadb/upgrades/11-rearm-leftover-history-sync-backfill.sql @@ -0,0 +1,10 @@ +-- v11 (compatible with v3+): Re-arm backfill for chats with leftover history sync messages +UPDATE backfill_task +SET is_done=false, queue_done=false, dispatched_at=NULL, completed_at=NULL +WHERE (is_done OR queue_done) AND EXISTS ( + SELECT 1 + FROM whatsapp_history_sync_message msg + WHERE msg.bridge_id=backfill_task.bridge_id + AND msg.user_login_id=backfill_task.user_login_id + AND msg.chat_jid=backfill_task.portal_id +); From ae8dcdec4bc4bf775e8b7185cc414c49ed8b48b2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 30 Oct 2026 16:09:08 +0200 Subject: [PATCH 119/139] backfill: delete all messages when FetchMessages returns no more history --- pkg/connector/backfill.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 378b58e..7a6fd78 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -593,10 +593,11 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet } } } - resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true) + resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true, !params.Forward && !hasMore) if err != nil { return nil, fmt.Errorf("failed to convert messages: %w", err) } + // TODO might be better to always set true here and let GetBetween return an empty result to mark as false? resp.HasMore = hasMore resp.Forward = params.Forward resp.MarkRead = markRead @@ -654,6 +655,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( portalJID types.JID, messages []*waWeb.WebMessageInfo, explodeOnError bool, + deleteEverything bool, ) (*bridgev2.FetchMessagesResponse, error) { cli := wa.Client if cli == nil { @@ -709,9 +711,11 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( Messages: convertedMessages, Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)), 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) + if deleteEverything { + wa.deleteHistorySyncMessages(ctx, portalJID, 1<<62, oldestTS) + } else { + wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS) + } if len(mediaRequests) > 0 { go func(ctx context.Context) { for _, req := range mediaRequests { @@ -872,7 +876,7 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w Int("message_count", len(messages)). Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()). Msg("Converting messages to bridge from on-demand history sync") - resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false) + resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false, false) if err != nil { return nil, err } From de02399ff8ec8d84d57955565979ccca07ba8b0a Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Fri, 30 Oct 2026 14:43:44 +0000 Subject: [PATCH 120/139] msgconv/from-whatsapp: show view once notice for media with view once flag (#1012) --- pkg/msgconv/wa-media.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go index 30c6b30..839f4c2 100644 --- a/pkg/msgconv/wa-media.go +++ b/pkg/msgconv/wa-media.go @@ -49,6 +49,9 @@ func (mc *MessageConverter) convertMediaMessage( isViewOnce bool, cachedPart *bridgev2.ConvertedMessagePart, ) (part *bridgev2.ConvertedMessagePart, contextInfo *waE2E.ContextInfo) { + if viewOnceMsg, ok := msg.(MediaMessageWithViewOnce); ok && viewOnceMsg.GetViewOnce() { + isViewOnce = true + } if mc.DisableViewOnce && isViewOnce { body := "You received a view once message. For added privacy, you can only open it on the WhatsApp app." if messageInfo.IsFromMe { @@ -232,6 +235,11 @@ type MediaMessageWithCaption interface { GetCaption() string } +type MediaMessageWithViewOnce interface { + MediaMessage + GetViewOnce() bool +} + type MediaMessageWithDimensions interface { MediaMessage GetHeight() uint32 From 5dd621fdf4232cd9ba837d0bcebac72fcb369884 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Fri, 30 Oct 2026 16:29:54 +0000 Subject: [PATCH 121/139] login: fix ADV secret rotation ending QR and pairing code logins (#1010) --- pkg/connector/login.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/connector/login.go b/pkg/connector/login.go index c5d4c0a..ae57ac1 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -288,6 +288,7 @@ func (wl *WALogin) handleEvent(rawEvt any) { wl.QRs[i] = strings.Replace(code, evt.OldSecret, evt.NewSecret, 1) } wl.ADVRotate.Set() + return case *events.QRScannedWithoutMultidevice: wl.Log.Error().Msg("QR code scanned without multidevice enabled") wl.LoginError = ErrLoginMultideviceNotEnabled @@ -372,9 +373,14 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { case <-wl.PasskeyRequest.GetChan(): return wl.makePasskeyStep() case <-wl.ADVRotate.GetChan(): - wl.Log.Debug().Msg("ADV secret was rotated, returning new QR immediately") wl.ADVRotate.Clear() - return makeQRStep(wl.QRs[nextIndex]), nil + if currentIndex < 0 || currentIndex >= len(wl.QRs) { + wl.Log.Debug().Msg("ADV secret was rotated, but no QRs are left to return") + wl.Cancel() + return nil, ErrLoginTimeout + } + wl.Log.Debug().Msg("ADV secret was rotated, returning current QR immediately") + return makeQRStep(wl.QRs[currentIndex]), nil case <-wl.LoginComplete.GetChan(): // continue } From 9f8224606920ae9d3aa5bbfc067e1cfe5d8b7475 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 1 Nov 2026 00:50:14 +0200 Subject: [PATCH 122/139] handlewhatsapp: exclude stream replaced and client outdated from unknown error autoreconnect Closes #1005 --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/handlewhatsapp.go | 12 ++++++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index b059b8b..6e9b503 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( golang.org/x/sync v0.24.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.32.1-0.20261028161347-0907c3c7611e + maunium.net/go/mautrix v0.32.1-0.20261031224934-b69deb427bd7 ) require ( diff --git a/go.sum b/go.sum index 498f286..12cf8f5 100644 --- a/go.sum +++ b/go.sum @@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.32.1-0.20261028161347-0907c3c7611e h1:7F0CBiL2wsWCfZ1gZlAc30fVeLKNMzuIQb9PZ2mPwts= -maunium.net/go/mautrix v0.32.1-0.20261028161347-0907c3c7611e/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= +maunium.net/go/mautrix v0.32.1-0.20261031224934-b69deb427bd7 h1:GpLIqeJTpG0/ti7XA370HgjnSWd/pQxXVGb/FRkayOA= +maunium.net/go/mautrix v0.32.1-0.20261031224934-b69deb427bd7/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 4ba4f10..a7d4c91 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -237,7 +237,11 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { }) wa.notifyOfflineSyncWaiter(fmt.Errorf("stream error: %s", message)) case *events.StreamReplaced: - wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAStreamReplaced}) + wa.UserLogin.BridgeState.Send(status.BridgeState{ + StateEvent: status.StateUnknownError, + Error: WAStreamReplaced, + BlockAutoReconnect: true, + }) wa.notifyOfflineSyncWaiter(fmt.Errorf("stream replaced")) case *events.KeepAliveTimeout: wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WAKeepaliveTimeout}) @@ -253,7 +257,11 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { wa.notifyOfflineSyncWaiter(fmt.Errorf("connection failure: %s (%s)", evt.Reason, evt.Message)) case *events.ClientOutdated: wa.UserLogin.Log.Error().Msg("Got a client outdated connect failure. The bridge is likely out of date, please update immediately.") - wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAClientOutdated}) + wa.UserLogin.BridgeState.Send(status.BridgeState{ + StateEvent: status.StateUnknownError, + Error: WAClientOutdated, + BlockAutoReconnect: true, + }) wa.notifyOfflineSyncWaiter(fmt.Errorf("client outdated")) case *events.TemporaryBan: wa.UserLogin.BridgeState.Send(status.BridgeState{ From 3a5d6448a8b4d7af1021381891f61c664084099c Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sun, 1 Nov 2026 12:58:51 +0000 Subject: [PATCH 123/139] backfill: treat own messages from previous LIDs as own (#1017) --- pkg/connector/backfill.go | 3 +++ pkg/connector/client.go | 25 ++++++++++++++++++++++++- pkg/waid/dbmeta.go | 2 ++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 7a6fd78..fe251e1 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -692,6 +692,9 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, false) { return nil, fmt.Errorf("failed to ensure alt JIDs for message %s", evt.Info.ID) } + if evt.Info.IsFromMe { + wa.addPreviousOwnLID(ctx, evt.Info.Sender) + } isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension converted, mediaReq := wa.convertHistorySyncMessage( ctx, cli, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 4181235..678c8d2 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -55,6 +55,7 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle), pushNamesSynced: exsync.NewEvent(), appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time), + previousOwnLIDs: exsync.NewSetWithItems(login.Metadata.(*waid.UserLoginMetadata).PreviousOwnLIDs), onDemandHistorySyncRequests: make(map[types.JID]onDemandHistorySyncRequest), @@ -138,6 +139,9 @@ type WhatsAppClient struct { onDemandHistorySyncLock sync.Mutex onDemandHistorySyncRequests map[types.JID]onDemandHistorySyncRequest + + previousOwnLIDs *exsync.Set[types.JID] + previousOwnLIDsLock sync.Mutex } var ( @@ -206,7 +210,26 @@ func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) func (wa *WhatsAppClient) IsOwnJID(jid types.JID) bool { return (jid.Server == types.DefaultUserServer && jid.User == wa.JID.User) || - (jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User) + (jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User) || + wa.previousOwnLIDs.Has(jid.ToNonAD()) +} + +func (wa *WhatsAppClient) addPreviousOwnLID(ctx context.Context, jid types.JID) { + if jid.Server != types.HiddenUserServer || wa.IsOwnJID(jid) { + return + } + jid = jid.ToNonAD() + wa.previousOwnLIDsLock.Lock() + defer wa.previousOwnLIDsLock.Unlock() + if !wa.previousOwnLIDs.Add(jid) { + return + } + zerolog.Ctx(ctx).Info().Stringer("lid", jid).Msg("Found previous own LID in history sync") + wa.UserLogin.Metadata.(*waid.UserLoginMetadata).PreviousOwnLIDs = wa.previousOwnLIDs.AsList() + err := wa.UserLogin.Save(ctx) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to save user login after adding previous own LID") + } } func (wa *WhatsAppClient) GetLID() types.JID { diff --git a/pkg/waid/dbmeta.go b/pkg/waid/dbmeta.go index 15c0a42..b1a47e0 100644 --- a/pkg/waid/dbmeta.go +++ b/pkg/waid/dbmeta.go @@ -45,6 +45,8 @@ type UserLoginMetadata struct { HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitzero"` ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitzero"` + PreviousOwnLIDs []types.JID `json:"previous_own_lids,omitempty"` + Blocklist *types.Blocklist `json:"blocklist,omitempty"` MData json.RawMessage `json:"mdata,omitempty"` From 07d8e9417b3451f3adb38c48e8047e273c64c6e8 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 2 Nov 2026 11:14:43 +0000 Subject: [PATCH 124/139] client: build whatsmeow HTTP clients from bridge settings (#1016) --- pkg/connector/connector.go | 11 +++++++++- pkg/connector/proxy.go | 45 +++++++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 19caace..80b9d95 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -243,7 +243,16 @@ func (wa *WhatsAppConnector) GenerateTransactionID(_ id.UserID, _ id.RoomID, _ e } func (wa *WhatsAppConnector) ResetHTTPTransport() { - // No-op for now, whatsmeow doesn't use the shared transport config yet + for _, login := range wa.Bridge.GetAllCachedUserLogins() { + client := login.Client.(*WhatsAppClient).Client + if client == nil { + continue + } + err := wa.updateProxy(login.Log.WithContext(wa.Bridge.BackgroundCtx), client, false) + if err != nil { + login.Log.Err(err).Msg("Failed to recreate HTTP clients") + } + } } func (wa *WhatsAppConnector) ResetNetworkConnections() { diff --git a/pkg/connector/proxy.go b/pkg/connector/proxy.go index fe1d7ff..8bd6d65 100644 --- a/pkg/connector/proxy.go +++ b/pkg/connector/proxy.go @@ -54,23 +54,38 @@ func (wa *WhatsAppConnector) getProxy(ctx context.Context, reason string) (strin } func (wa *WhatsAppConnector) updateProxy(ctx context.Context, client *whatsmeow.Client, isLogin bool) error { - if wa.Config.ProxyOnlyLogin && !isLogin { - return nil + var proxy string + if !wa.Config.ProxyOnlyLogin || isLogin { + reason := "connect" + if isLogin { + reason = "login" + } + var err error + proxy, err = wa.getProxy(ctx, reason) + if err != nil { + return fmt.Errorf("failed to get proxy address: %w", err) + } } - reason := "connect" - if isLogin { - reason = "login" + settings := wa.Bridge.GetHTTPClientSettings().WithGlobalTimeout(0) + if settings.HTTPProxy == nil { + settings.HTTPProxy = http.ProxyFromEnvironment } - if proxy, err := wa.getProxy(ctx, reason); err != nil { - return fmt.Errorf("failed to get proxy address: %w", err) - } else if proxy == "" { - return nil - } else if err = client.SetProxyAddress(proxy, whatsmeow.SetProxyOptions{ - OnlyLogin: wa.Config.ProxyOnlyLogin, - NoMedia: wa.Config.ProxyOnlyLogin, - }); err != nil { - return fmt.Errorf("failed to set proxy address: %w", err) + proxySettings := settings + if proxy != "" { + var err error + proxySettings, err = settings.WithProxy(proxy) + if err != nil { + return fmt.Errorf("failed to set proxy address: %w", err) + } + } + client.SetPreLoginHTTPClient(proxySettings.Compile()) + if wa.Config.ProxyOnlyLogin { + proxySettings = settings + } + client.SetWebsocketHTTPClient(proxySettings.Compile()) + client.SetMediaHTTPClient(proxySettings.WithResponseHeaderTimeout(0).Compile()) + if proxy != "" { + zerolog.Ctx(ctx).Debug().Msg("Enabled proxy") } - zerolog.Ctx(ctx).Debug().Msg("Enabled proxy") return nil } From 96ce0799be8c962d64cde63ab383a925d7a8fd84 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 2 Nov 2026 12:57:03 +0000 Subject: [PATCH 125/139] handlewhatsapp: don't retry full sync after a recovery response (#1018) --- pkg/connector/handlewhatsapp.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index a7d4c91..4391233 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -970,7 +970,9 @@ func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt defer wa.appStateRecoveryLock.Unlock() meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata) if ts, exists := meta.AppStateRecoveryAttempted[evt.Name]; exists { - delete(wa.appStateFullSyncAttempted, evt.Name) + if !evt.Recovery { + delete(wa.appStateFullSyncAttempted, evt.Name) + } delete(meta.AppStateRecoveryAttempted, evt.Name) err := wa.UserLogin.Save(ctx) if err != nil { @@ -1008,7 +1010,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev return } if !evt.FullSync { - if !lastFullSync.IsZero() { + if !lastFullSync.IsZero() && time.Since(lastFullSync) < 48*time.Hour { log.Debug(). Err(evt.Error). Time("last_full_sync_attempt", lastFullSync). From 4be45ddbea7d2aec2fe977cf3ea2d62c485a3a47 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 09:28:04 -0500 Subject: [PATCH 126/139] handlewhatsapp: send media retry edits as the original message sender (#1019) Co-authored-by: Indent --- pkg/connector/events.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/connector/events.go b/pkg/connector/events.go index 481d0d7..7f906e8 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -474,7 +474,9 @@ func (evt *WAMediaRetry) getRealSender() types.JID { func (evt *WAMediaRetry) GetSender() bridgev2.EventSender { realSender := pickLID(evt.getRealSender(), evt.senderLID) - return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender) + sender := evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender) + sender.ForceEditOrigSender = true + return sender } func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID { From e097ba38de1f5d450013eb52b3136f28c7131dea Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 2 Nov 2026 15:14:20 +0000 Subject: [PATCH 127/139] userinfo: don't copy unauthorized avatar to alt ghost (#1020) --- pkg/connector/userinfo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index b109e82..3d5499c 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -480,7 +480,7 @@ func makeInfoFromGhost(ghost *bridgev2.Ghost) *bridgev2.UserInfo { IsBot: &ghost.IsBot, ExtraProfile: ghost.ExtraProfile, } - if ghost.AvatarID != "" { + if ghost.AvatarID != "" && ghost.AvatarID != "unauthorized" { info.Avatar = &bridgev2.Avatar{ ID: ghost.AvatarID, Remove: ghost.AvatarMXC == "", From 78c5ee3b207d982cab19bec7424279e3fc365071 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:41:06 -0500 Subject: [PATCH 128/139] backfill: expire unanswered on-demand history sync requests after an hour (#1021) Co-authored-by: Indent --- pkg/connector/backfill.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index fe251e1..343b240 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -799,10 +799,12 @@ type onDemandHistorySyncRequest struct { SentAt time.Time } +const onDemandHistorySyncRequestTimeout = 1 * time.Hour + func (wa *WhatsAppClient) startOnDemandHistorySyncRequest(ctx context.Context, portalJID types.JID, msgID types.MessageID) bool { wa.onDemandHistorySyncLock.Lock() defer wa.onDemandHistorySyncLock.Unlock() - if req, inFlight := wa.onDemandHistorySyncRequests[portalJID]; inFlight { + if req, inFlight := wa.onDemandHistorySyncRequests[portalJID]; inFlight && time.Since(req.SentAt) < onDemandHistorySyncRequestTimeout { zerolog.Ctx(ctx).Debug(). Str("in_flight_request_msg_id", req.ID). Stringer("portal_jid", portalJID). From 429320b373d2154cfdcca3e6981e2af9f7f00ee1 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Tue, 3 Nov 2026 21:35:54 +0000 Subject: [PATCH 129/139] pre-commit: add bridgelint hook (#1022) --- .pre-commit-config.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bfb536..d6c78bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,9 +21,11 @@ repos: - id: go-mod-tidy - repo: https://github.com/beeper/pre-commit-go - rev: v0.4.2 + rev: v0.5.0 hooks: - id: zerolog-ban-msgf - id: zerolog-use-stringer - id: prevent-literal-http-methods - id: zerolog-ban-global-log + - id: bridgelint + verbose: true From 5ba69ab4ee9c916c488ada28c6086d7ec382225c Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Wed, 4 Nov 2026 16:24:07 +0000 Subject: [PATCH 130/139] backfill: drop history sync notifications whose blob is gone (#1023) --- pkg/connector/backfill.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 343b240..cbc7eec 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -142,8 +142,16 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, Str("peer_data_request_session_id", evt.GetPeerDataRequestSessionID()). Msg("Downloading history sync") blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true) - if err != nil { + if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) { + log.Err(err).Msg("History sync blob is no longer available, dropping notification") + if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil { + log.Err(err).Msg("Failed to delete queued history sync notification") + errored = true + } + return + } else if err != nil { log.Err(err).Msg("Failed to download history sync") + errored = true return } if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND { @@ -171,6 +179,7 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, }) if err != nil { log.Err(err).Msg("Failed to store history sync notification data") + errored = true } else { resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP || blob.GetSyncType() == waHistorySync.HistorySync_RECENT || From 1d024590d2df575f1f2e184229ace51b0300ff70 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:16:41 -0500 Subject: [PATCH 131/139] userinfo: don't queue @bot ghosts for background resync (#1024) Co-authored-by: Indent --- pkg/connector/userinfo.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index 3d5499c..b659453 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -29,12 +29,15 @@ var ResyncLoopInterval = 4 * time.Hour var ResyncJitterSeconds = 3600 func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) { + jid := waid.ParseUserID(ghost.ID) + if jid.Server == types.BotServer { + return + } lastSync := ghost.Metadata.(*waid.GhostMetadata).LastSync.Time if ghost.AvatarID != "" && lastSync.Add(ResyncMinInterval).After(time.Now()) { return } wa.resyncQueueLock.Lock() - jid := waid.ParseUserID(ghost.ID) if _, exists := wa.resyncQueue[jid]; !exists { wa.resyncQueue[jid] = resyncQueueItem{ghost: ghost} nextResyncIn := time.Until(wa.nextResync).String() From 03859bb3db0b7f597bf93f2f35a7306879b85dcc Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Thu, 5 Nov 2026 13:09:38 +0000 Subject: [PATCH 132/139] pre-commit: install bridgelint with the CI Go toolchain (#1030) --- .github/workflows/go.yml | 1 + .pre-commit-config.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c9fef73..146b93a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -30,6 +30,7 @@ jobs: run: | go install golang.org/x/tools/cmd/goimports@latest go install honnef.co/go/tools/cmd/staticcheck@latest + go install github.com/beeper/pre-commit-go/bridgelint@latest export PATH="$HOME/go/bin:$PATH" - name: Run pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d6c78bb..1c96b06 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,4 +28,5 @@ repos: - id: prevent-literal-http-methods - id: zerolog-ban-global-log - id: bridgelint + language: system verbose: true From 81f64557ea4f8535a4323135f42569b8e26646a5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 5 Nov 2026 09:16:26 -0500 Subject: [PATCH 133/139] handlematrix: ignore thread read receipts Closes #1028 --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/handlematrix.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 6e9b503..f940c1d 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( golang.org/x/sync v0.24.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.32.1-0.20261031224934-b69deb427bd7 + maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445 ) require ( diff --git a/go.sum b/go.sum index 12cf8f5..fb4e878 100644 --- a/go.sum +++ b/go.sum @@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.32.1-0.20261031224934-b69deb427bd7 h1:GpLIqeJTpG0/ti7XA370HgjnSWd/pQxXVGb/FRkayOA= -maunium.net/go/mautrix v0.32.1-0.20261031224934-b69deb427bd7/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= +maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445 h1:ZI9Y2YEmjqQDloOCiciHeynTRYjUwK0ZZbNcRH/5NOc= +maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index d51efcd..8419850 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -319,7 +319,7 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br } func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) { - if !receipt.ReadUpTo.After(receipt.LastRead) { + if receipt.Receipt.IsThread() || !receipt.ReadUpTo.After(receipt.LastRead) { return nil } if receipt.LastRead.IsZero() { From 5640ef311b5aef90ee0d35766ec9c9bf167d25af Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Thu, 5 Nov 2026 15:00:20 +0000 Subject: [PATCH 134/139] muse: mark WASA resync as done on any error (#1026) --- pkg/connector/muse.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/connector/muse.go b/pkg/connector/muse.go index ee6b04d..4a589ce 100644 --- a/pkg/connector/muse.go +++ b/pkg/connector/muse.go @@ -4,7 +4,6 @@ import ( "context" "encoding/base64" "encoding/json" - "errors" "fmt" "runtime/debug" "time" @@ -57,9 +56,9 @@ func (wa *WhatsAppClient) resyncWASARootSecrets(ctx context.Context) { success := true if err := wa.Client.FetchAppState(ctx, appstate.WAPatchRegularHigh, true, false); err != nil { log.Err(err).Msg("Failed to resync WASA root secrets") - // Mark the sync as completed on mismatching LTHash errors, because those can get permanently stuck - // (retrying won't help). In case it's not stuck, a single call should be enough to request a resync. - if !errors.Is(err, appstate.ErrMismatchingLTHash) || ctx.Err() != nil { + // Mark the sync as completed on errors too, because some get permanently stuck (mismatching LTHash, + // expired patch blobs), and a failed full sync already requests app state recovery from the phone. + if ctx.Err() != nil { return } success = false From d574a67a2d22a15af72d953ff5c2d44adfe8562c Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:33:02 +0000 Subject: [PATCH 135/139] userinfo: query only the LID when both JIDs of a contact are queued for background resync (#1029) Co-authored-by: Indent --- pkg/connector/userinfo.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index b659453..387971e 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -119,6 +119,7 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID var ghostJIDs []types.JID var ghosts []*bridgev2.Ghost var portals []*bridgev2.Portal + selectedGhosts := make(map[types.JID]*bridgev2.Ghost) for jid, item := range queue { var lastSync time.Time unknownAvatar := false @@ -136,12 +137,23 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID continue } if item.ghost != nil { - ghosts = append(ghosts, item.ghost) - ghostJIDs = append(ghostJIDs, jid) + selectedGhosts[jid] = item.ghost } else if item.portal != nil { portals = append(portals, item.portal) } } + for jid, ghost := range selectedGhosts { + if jid.Server == types.DefaultUserServer { + lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) + if err != nil { + log.Warn().Err(err).Stringer("jid", jid).Msg("Failed to get LID for background sync") + } else if _, lidSelected := selectedGhosts[lid]; lidSelected { + continue + } + } + ghosts = append(ghosts, ghost) + ghostJIDs = append(ghostJIDs, jid) + } for _, portal := range portals { wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ EventMeta: simplevent.EventMeta{ From ac29fa5272ed896586320f92dfa63d305883f895 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Fri, 6 Nov 2026 20:37:55 +0000 Subject: [PATCH 136/139] lidmigrate: check homeserver membership when removing rejoined PN ghosts (#1025) --- pkg/connector/lidmigrate.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pkg/connector/lidmigrate.go b/pkg/connector/lidmigrate.go index 17b2428..4295be6 100644 --- a/pkg/connector/lidmigrate.go +++ b/pkg/connector/lidmigrate.go @@ -300,7 +300,11 @@ func (wa *WhatsAppConnector) syncMismatchingGhosts(ctx context.Context) error { } func (wa *WhatsAppConnector) removeRejoinedPNGhosts(ctx context.Context) error { - if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dm_pn_ghosts_removed") == "true" { + if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dm_pn_ghosts_removed_v2") == "true" { + return nil + } + stateGetter, ok := wa.Bridge.Matrix.(bridgev2.MatrixConnectorWithArbitraryRoomState) + if !ok { return nil } log := zerolog.Ctx(ctx).With().Str("action", "remove rejoined pn ghosts").Logger() @@ -322,12 +326,14 @@ func (wa *WhatsAppConnector) removeRejoinedPNGhosts(ctx context.Context) error { continue } pnUserID := waid.MakeUserID(pn) - member, err := wa.Bridge.Matrix.GetMemberInfo(ctx, portal.MXID, wa.Bridge.Matrix.FormatGhostMXID(pnUserID)) - if err != nil { + memberEvt, err := stateGetter.GetStateEvent(ctx, portal.MXID, event.StateMember, wa.Bridge.Matrix.FormatGhostMXID(pnUserID).String()) + if errors.Is(err, mautrix.MNotFound) || errors.Is(err, mautrix.MForbidden) { + continue + } else if err != nil { log.Warn().Err(err).Stringer("room_id", portal.MXID).Msg("Failed to get PN ghost membership") failed++ continue - } else if member == nil || member.Membership != event.MembershipJoin { + } else if memberEvt == nil || memberEvt.Content.AsMember().Membership != event.MembershipJoin { continue } pnGhost, err := wa.Bridge.GetGhostByID(ctx, pnUserID) @@ -345,6 +351,12 @@ func (wa *WhatsAppConnector) removeRejoinedPNGhosts(ctx context.Context) error { } continue } + log.Debug(). + Stringer("room_id", portal.MXID). + Stringer("pn", pn). + Stringer("join_sender", memberEvt.Sender). + Time("join_ts", time.UnixMilli(memberEvt.Timestamp)). + Msg("Removed PN ghost from LID DM") removed++ } log.Info(). @@ -353,7 +365,7 @@ func (wa *WhatsAppConnector) removeRejoinedPNGhosts(ctx context.Context) error { Int("total_lid_portals", len(lidPortals)). Msg("Finished removing rejoined PN ghosts from LID DMs") if failed == 0 && ctx.Err() == nil { - wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dm_pn_ghosts_removed", "true") + wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dm_pn_ghosts_removed_v2", "true") } return nil } From d9b0daca891ac69226931eba5594e0f490aaee51 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:46:19 -0500 Subject: [PATCH 137/139] msgconv/rich-response: only embed raw rich message JSON if it's canonical (#1033) Co-authored-by: Indent --- pkg/msgconv/wa-rich-response.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/msgconv/wa-rich-response.go b/pkg/msgconv/wa-rich-response.go index 28a648c..e506a64 100644 --- a/pkg/msgconv/wa-rich-response.go +++ b/pkg/msgconv/wa-rich-response.go @@ -29,6 +29,7 @@ import ( "github.com/yuin/goldmark/extension" "go.mau.fi/whatsmeow/types/richresponse" "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/crypto/canonicaljson" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format/mdext" @@ -36,7 +37,8 @@ import ( func (mc *MessageConverter) convertUnifiedRichResponseMessage(ctx context.Context, data []byte) *bridgev2.ConvertedMessagePart { extra := map[string]any{} - if json.Valid(data) && len(data) < 24*1024 { + _, canonicalErr := canonicaljson.Marshal(json.RawMessage(data)) + if canonicalErr == nil && len(data) < 24*1024 { extra["fi.mau.whatsapp.rich_message"] = json.RawMessage(data) } else if len(data) < 16*1024 { extra["fi.mau.whatsapp.non_json_rich_message"] = base64.StdEncoding.EncodeToString(data) From 5c8c1fc8358c011891fbf6066f289b9ad5fee223 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:09:28 +0000 Subject: [PATCH 138/139] handlewhatsapp: ignore HD replacement messages without replacement content (#1037) Co-authored-by: Indent --- pkg/connector/handlewhatsapp.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 4391233..e6174b9 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -374,6 +374,14 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD { parentKey := messageAssoc.GetParentMessageKey() protocolMsg, shouldHideEdit := makeHDMediaReplacementEdit(evt.Message, parentKey) + if protocolMsg.GetEditedMessage() == nil { + wa.UserLogin.Log.Debug(). + Str("message_id", evt.Info.ID). + Str("parent_id", parentKey.GetID()). + Stringer("assoc_type", assocType). + Msg("Ignoring HD replacement message without replacement content") + return + } dontRenderEdited = shouldHideEdit wa.UserLogin.Log.Debug(). Str("message_id", evt.Info.ID). From 945474343fcd7579c3d273acb78f6c60c957e2e0 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:09:56 +0000 Subject: [PATCH 139/139] backfill: log stub and message type of unparseable history messages (#1034) Co-authored-by: Indent --- pkg/connector/backfill.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index cbc7eec..8f006e0 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -298,6 +298,9 @@ func (wa *WhatsAppClient) handleWAHistorySync( Int("msg_index", i). Str("msg_id", rawMsg.GetMessage().GetKey().GetID()). Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()). + Stringer("stub_type", rawMsg.GetMessage().GetMessageStubType()). + Str("message_type", getMessageType(rawMsg.GetMessage().GetMessage())). + Bool("from_me", rawMsg.GetMessage().GetKey().GetFromMe()). Msg("Dropping historical message due to parse error") log.Trace(). Any("web_message_info", rawMsg.GetMessage()).