From 7ee5aa647cd8a35881b4ea2d4e6c0f9067e48097 Mon Sep 17 00:00:00 2001
From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com>
Date: Thu, 26 Mar 2026 22:02:48 -0700
Subject: [PATCH 01/94] -
---
pkg/connector/wamsgtype.go | 2 +
pkg/msgconv/from-whatsapp.go | 4 ++
pkg/msgconv/wa-misc.go | 81 ++++++++++++++++++++++++++++++++++++
3 files changed, 87 insertions(+)
diff --git a/pkg/connector/wamsgtype.go b/pkg/connector/wamsgtype.go
index 92e7f86..e716f5d 100644
--- a/pkg/connector/wamsgtype.go
+++ b/pkg/connector/wamsgtype.go
@@ -132,6 +132,8 @@ func getMessageType(waMsg *waE2E.Message) string {
return "poll result snapshot"
case waMsg.MessageHistoryBundle != nil:
return "message history bundle"
+ case waMsg.MessageHistoryNotice != nil:
+ return "message history notice"
case waMsg.RequestPhoneNumberMessage != nil:
return "request phone number"
case waMsg.KeepInChatMessage != nil:
diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go
index 11569e2..2117535 100644
--- a/pkg/msgconv/from-whatsapp.go
+++ b/pkg/msgconv/from-whatsapp.go
@@ -217,6 +217,10 @@ func (mc *MessageConverter) ToMatrix(
part, contextInfo = mc.convertGroupInviteMessage(ctx, info, waMsg.GroupInviteMessage)
case waMsg.ProtocolMessage != nil && waMsg.ProtocolMessage.GetType() == waE2E.ProtocolMessage_EPHEMERAL_SETTING:
part, contextInfo = mc.convertEphemeralSettingMessage(ctx, waMsg.ProtocolMessage, info.Timestamp, isBackfill)
+ case waMsg.MessageHistoryBundle != nil:
+ part, contextInfo = mc.convertMessageHistoryShare(ctx, info, waMsg.MessageHistoryBundle.GetMessageHistoryMetadata(), waMsg.MessageHistoryBundle.GetContextInfo())
+ case waMsg.MessageHistoryNotice != nil:
+ part, contextInfo = mc.convertMessageHistoryShare(ctx, info, waMsg.MessageHistoryNotice.GetMessageHistoryMetadata(), waMsg.MessageHistoryNotice.GetContextInfo())
case waMsg.EncCommentMessage != nil:
part = failedCommentPart
default:
diff --git a/pkg/msgconv/wa-misc.go b/pkg/msgconv/wa-misc.go
index 7ca4427..9222f78 100644
--- a/pkg/msgconv/wa-misc.go
+++ b/pkg/msgconv/wa-misc.go
@@ -21,6 +21,7 @@ import (
"encoding/base64"
"fmt"
"html/template"
+ "slices"
"strings"
"time"
@@ -73,6 +74,86 @@ func (mc *MessageConverter) convertPlaceholderMessage(ctx context.Context, rawMs
}
}
+func joinNaturalNames(names []string) string {
+ names = slices.DeleteFunc(names, func(name string) bool {
+ return name == ""
+ })
+ switch len(names) {
+ case 0:
+ return ""
+ case 1:
+ return names[0]
+ case 2:
+ return names[0] + " and " + names[1]
+ default:
+ return strings.Join(names[:len(names)-1], ", ") + ", and " + names[len(names)-1]
+ }
+}
+
+func (mc *MessageConverter) getHistoryReceiverName(ctx context.Context, receiver string) string {
+ jid, err := types.ParseJID(receiver)
+ if err != nil {
+ zerolog.Ctx(ctx).Err(err).Str("receiver_jid", receiver).Msg("Failed to parse message history receiver JID")
+ return receiver
+ }
+ _, displayname, err := mc.getBasicUserInfo(ctx, jid)
+ if err != nil {
+ zerolog.Ctx(ctx).Err(err).Stringer("receiver_jid", jid).Msg("Failed to resolve message history receiver")
+ if jid.User != "" {
+ return jid.User
+ }
+ return receiver
+ }
+ return displayname
+}
+
+func messageHistoryStartTime(metadata *waE2E.MessageHistoryMetadata, fallback time.Time) time.Time {
+ if metadata == nil {
+ return fallback
+ }
+ if ts := metadata.GetOldestMessageTimestamp(); ts > 0 {
+ return time.Unix(ts, 0).Local()
+ }
+ return fallback
+}
+
+func (mc *MessageConverter) convertMessageHistoryShare(ctx context.Context, info *types.MessageInfo, metadata *waE2E.MessageHistoryMetadata, contextInfo *waE2E.ContextInfo) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
+ names := make([]string, 0, len(metadata.GetHistoryReceivers()))
+ for _, receiver := range metadata.GetHistoryReceivers() {
+ names = append(names, mc.getHistoryReceiverName(ctx, receiver))
+ }
+
+ receivers := joinNaturalNames(names)
+ var fallback time.Time
+ if info != nil {
+ fallback = info.Timestamp
+ }
+ startAt := messageHistoryStartTime(metadata, fallback)
+ body := "Message history shared."
+ if !startAt.IsZero() {
+ startTime := startAt.Format("Jan 2, 2006 at 3:04 PM")
+ body = fmt.Sprintf("Message history shared starting on %s.", startTime)
+ switch {
+ case info != nil && info.IsFromMe && receivers != "":
+ body = fmt.Sprintf("You sent %s message history that starts on %s.", receivers, startTime)
+ case receivers != "":
+ body = fmt.Sprintf("Sent %s message history that starts on %s.", receivers, startTime)
+ }
+ } else if info != nil && info.IsFromMe && receivers != "" {
+ body = fmt.Sprintf("You sent %s message history.", receivers)
+ } else if receivers != "" {
+ body = fmt.Sprintf("Sent %s message history.", receivers)
+ }
+
+ return &bridgev2.ConvertedMessagePart{
+ Type: event.EventMessage,
+ Content: &event.MessageEventContent{
+ MsgType: event.MsgNotice,
+ Body: body,
+ },
+ }, contextInfo
+}
+
const inviteMsg = `%s
This invitation to join "%s" expires at %s. Reply to this message with %s accept to accept the invite.`
const inviteMsgBroken = `%s
This invitation to join "%s" expires at %s. However, the invite message is broken or unsupported and cannot be accepted.`
const GroupInviteMetaField = "fi.mau.whatsapp.invite"
From 5a7217e0ffe5c6c126335fc86c5cc80be1a27eb7 Mon Sep 17 00:00:00 2001
From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com>
Date: Fri, 27 Mar 2026 09:10:36 -0700
Subject: [PATCH 02/94] -
---
pkg/msgconv/wa-misc.go | 23 ++++-------------------
1 file changed, 4 insertions(+), 19 deletions(-)
diff --git a/pkg/msgconv/wa-misc.go b/pkg/msgconv/wa-misc.go
index 9222f78..22c014c 100644
--- a/pkg/msgconv/wa-misc.go
+++ b/pkg/msgconv/wa-misc.go
@@ -21,7 +21,6 @@ import (
"encoding/base64"
"fmt"
"html/template"
- "slices"
"strings"
"time"
@@ -74,22 +73,6 @@ func (mc *MessageConverter) convertPlaceholderMessage(ctx context.Context, rawMs
}
}
-func joinNaturalNames(names []string) string {
- names = slices.DeleteFunc(names, func(name string) bool {
- return name == ""
- })
- switch len(names) {
- case 0:
- return ""
- case 1:
- return names[0]
- case 2:
- return names[0] + " and " + names[1]
- default:
- return strings.Join(names[:len(names)-1], ", ") + ", and " + names[len(names)-1]
- }
-}
-
func (mc *MessageConverter) getHistoryReceiverName(ctx context.Context, receiver string) string {
jid, err := types.ParseJID(receiver)
if err != nil {
@@ -120,10 +103,12 @@ func messageHistoryStartTime(metadata *waE2E.MessageHistoryMetadata, fallback ti
func (mc *MessageConverter) convertMessageHistoryShare(ctx context.Context, info *types.MessageInfo, metadata *waE2E.MessageHistoryMetadata, contextInfo *waE2E.ContextInfo) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
names := make([]string, 0, len(metadata.GetHistoryReceivers()))
for _, receiver := range metadata.GetHistoryReceivers() {
- names = append(names, mc.getHistoryReceiverName(ctx, receiver))
+ if name := mc.getHistoryReceiverName(ctx, receiver); name != "" {
+ names = append(names, name)
+ }
}
- receivers := joinNaturalNames(names)
+ receivers := strings.Join(names, ", ")
var fallback time.Time
if info != nil {
fallback = info.Timestamp
From 21514cd218b01c81e36d3c9a2474d6957836d1fd Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Tue, 31 Mar 2026 19:34:11 +0300
Subject: [PATCH 03/94] dependencies: update mautrix-go
---
go.mod | 14 +++++++-------
go.sum | 36 ++++++++++++++---------------------
pkg/connector/handlematrix.go | 4 ++++
3 files changed, 25 insertions(+), 29 deletions(-)
diff --git a/go.mod b/go.mod
index e8f8bb1..a28f7c7 100644
--- a/go.mod
+++ b/go.mod
@@ -7,30 +7,30 @@ toolchain go1.26.1
tool go.mau.fi/util/cmd/maubuild
require (
- github.com/lib/pq v1.11.2
- github.com/rs/zerolog v1.34.0
+ github.com/lib/pq v1.12.0
+ github.com/rs/zerolog v1.35.0
go.mau.fi/util v0.9.7
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4
+ go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4
golang.org/x/image v0.37.0
golang.org/x/net v0.52.0
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7
+ maunium.net/go/mautrix v0.26.5-0.20260331163037-18917f3bdc14
)
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/beeper/argo-go v1.1.2 // indirect
github.com/coder/websocket v1.8.14 // indirect
- github.com/coreos/go-systemd/v22 v22.6.0 // indirect
+ github.com/coreos/go-systemd/v22 v22.7.0 // indirect
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mattn/go-sqlite3 v1.14.34 // indirect
+ github.com/mattn/go-sqlite3 v1.14.37 // indirect
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect
@@ -40,7 +40,7 @@ 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.7.16 // indirect
+ github.com/yuin/goldmark v1.8.2 // indirect
go.mau.fi/libsignal v0.2.1 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
diff --git a/go.sum b/go.sum
index 6fedc40..ff1245a 100644
--- a/go.sum
+++ b/go.sum
@@ -10,15 +10,13 @@ github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
-github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
-github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
+github.com/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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -30,21 +28,17 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
-github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo=
+github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
-github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
+github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 h1:rh2lKw/P/EqHa724vYH2+VVQ1YnW4u6EOXl0PMAovZE=
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
@@ -52,8 +46,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
-github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
-github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
+github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
+github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
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=
@@ -73,16 +67,16 @@ 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.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
-github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
+github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
+github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
go.mau.fi/util v0.9.7 h1:AWGNbJfz1zRcQOKeOEYhKUG2fT+/26Gy6kyqcH8tnBg=
go.mau.fi/util v0.9.7/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4 h1:FGA3NtCVNeCJ+C+KBg1pODsrfxC/trM3RHFWIeY7y4c=
-go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM=
+go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4 h1:E4A6eca9vMJQctC9DIfzUIg27TrJ8IrDHgkJwJ8WPUQ=
+go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
@@ -97,9 +91,7 @@ golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
@@ -115,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.26.5-0.20260322102453-0c955c396df7 h1:KUhlBHWGgknqYC2V8di4DFNh73atDtgPlqqO5FoLmPc=
-maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7/go.mod h1:YWw8NWTszsbyFAznboicBObwHPgTSLcuTbVX2kY7U2M=
+maunium.net/go/mautrix v0.26.5-0.20260331163037-18917f3bdc14 h1:y+4gtqKBMTtcVUiAeWJnvp88JLo/h3myQPsz1rZfNOY=
+maunium.net/go/mautrix v0.26.5-0.20260331163037-18917f3bdc14/go.mod h1:RUSMBPky3jhXB7Ux+AptfkEvFlJ4ajZKCYiXI8YzxVE=
diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go
index a330a98..a0a67fa 100644
--- a/pkg/connector/handlematrix.go
+++ b/pkg/connector/handlematrix.go
@@ -391,6 +391,10 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
}
func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
+ if msg.Type.IsSelf && msg.OrigSender != nil {
+ return nil, nil
+ }
+
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return nil, err
From e2869f2a1d968217d21e20416231bbe4ddf4b62b Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 2 Apr 2026 15:08:00 +0300
Subject: [PATCH 04/94] backfill: add support for on-demand backwards backfill
requests
---
pkg/connector/backfill.go | 238 +++++++++++++++++++++++++-----
pkg/connector/config.go | 3 +
pkg/connector/example-config.yaml | 3 +
3 files changed, 206 insertions(+), 38 deletions(-)
diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go
index 21d0c4d..f33351f 100644
--- a/pkg/connector/backfill.go
+++ b/pkg/connector/backfill.go
@@ -115,12 +115,28 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
Uint32("chunk_order", evt.GetChunkOrder()).
Uint32("progress", evt.GetProgress()).
Logger()
- log.Debug().Msg("Downloading history sync")
+ log.Debug().
+ Int64("oldest_msg_in_chunk_ts", evt.GetOldestMsgInChunkTimestampSec()).
+ Any("full_request_meta", evt.GetFullHistorySyncOnDemandRequestMetadata()).
+ Any("access_status", evt.GetMessageAccessStatus()).
+ Str("peer_data_request_session_id", evt.GetPeerDataRequestSessionID()).
+ Msg("Downloading history sync")
blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true)
if err != nil {
log.Err(err).Msg("Failed to download history sync")
return
}
+ if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND {
+ wa.handleOnDemandHistorySync(ctx, blob)
+ if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil {
+ log.Err(err).Msg("Failed to delete queued on-demand history sync notification")
+ } else if err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle()); err != nil {
+ log.Err(err).Msg("Failed to delete history sync blob from server")
+ } else {
+ log.Debug().Msg("Finished handling on-demand history sync and deleted history sync blob from server")
+ }
+ return
+ }
err = wa.Main.DB.DoTxn(ctx, nil, func(ctx context.Context) (innerErr error) {
resetTimer, innerErr = wa.handleWAHistorySync(ctx, evt, blob, true)
if innerErr != nil {
@@ -134,6 +150,13 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
})
if err != nil {
log.Err(err).Msg("Failed to store history sync notification data")
+ } else {
+ err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle())
+ if err != nil {
+ log.Err(err).Msg("Failed to delete history sync blob from server")
+ } else {
+ log.Debug().Msg("Deleted history sync blob from server")
+ }
}
return
}
@@ -178,7 +201,8 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Dict("notification_metadata", zerolog.Dict().
Int64("oldest_msg_in_chunk_ts", notif.GetOldestMsgInChunkTimestampSec()).
Any("full_request_meta", notif.GetFullHistorySyncOnDemandRequestMetadata()).
- Any("access_status", notif.GetMessageAccessStatus())).
+ Any("access_status", notif.GetMessageAccessStatus()).
+ Str("peer_data_request_session_id", notif.GetPeerDataRequestSessionID())).
Msg("Storing history sync")
start := time.Now()
successfullySavedTotal := 0
@@ -275,7 +299,9 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Bool("archived", conv.GetArchived()).
Uint32("pinned", conv.GetPinned()).
Uint64("mute_end", conv.GetMuteEndTime()).
- Uint32("unread_count", conv.GetUnreadCount()),
+ Uint32("unread_count", conv.GetUnreadCount()).
+ Bool("end_of_history", conv.GetEndOfHistoryTransfer()).
+ Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()),
).
Msg("Collected messages to save from history sync conversation")
@@ -439,24 +465,34 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
}
var markRead bool
var startTime, endTime *time.Time
+ var conv *wadb.Conversation
+ if params.Forward || wa.Main.Config.HistorySync.BackwardsOnDemand {
+ conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get conversation from database: %w", err)
+ }
+ }
if params.Forward {
if params.AnchorMessage != nil {
startTime = ptr.Ptr(params.AnchorMessage.Timestamp)
}
- conv, err := wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
- if err != nil {
- return nil, fmt.Errorf("failed to get conversation from database: %w", err)
- } else if conv != nil {
+ if conv != nil {
markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0
}
- } else if params.Cursor != "" {
- endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
- if err != nil {
- return nil, fmt.Errorf("failed to parse cursor: %w", err)
+ } else {
+ if params.AnchorMessage != nil {
+ endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
+ }
+ if params.Cursor != "" {
+ endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse cursor: %w", err)
+ }
+ cursorTime := time.Unix(endTimeUnix, 0)
+ if endTime == nil || cursorTime.Before(*endTime) {
+ endTime = &cursorTime
+ }
}
- endTime = ptr.Ptr(time.Unix(endTimeUnix, 0))
- } else if params.AnchorMessage != nil {
- endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
}
var anchorID types.MessageID
if params.AnchorMessage != nil {
@@ -465,19 +501,24 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
anchorID = parsedID.ID
}
}
+ var hasMore bool
+ if !params.Forward && wa.Main.Config.HistorySync.BackwardsOnDemand {
+ hasMore = conv != nil && ptr.Val(conv.EndOfHistoryTransferType) == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
+ }
messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1)
if err != nil {
return nil, fmt.Errorf("failed to load messages from database: %w", err)
} else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) {
+ if hasMore {
+ return wa.fetchMessagesFromPhone(ctx, params)
+ }
return &bridgev2.FetchMessagesResponse{
HasMore: false,
Forward: params.Forward,
}, nil
}
- hasMore := false
- oldestTS := messages[len(messages)-1].GetMessageTimestamp()
- newestTS := messages[0].GetMessageTimestamp()
if len(messages) > params.Count {
+ oldestTS := messages[len(messages)-1].GetMessageTimestamp()
hasMore = true
// For safety, cut off messages with the oldest timestamp in the response.
// Otherwise, if there are multiple messages with the same timestamp, the next fetch may miss some.
@@ -488,19 +529,66 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
}
}
}
- convertedMessages := make([]*bridgev2.BackfillMessage, len(messages))
+ resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true)
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert messages: %w", err)
+ }
+ resp.HasMore = hasMore
+ resp.Forward = params.Forward
+ resp.MarkRead = markRead
+ return resp, nil
+}
+
+func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) {
+ var err error
+ if (newestTS == 0 && oldestTS == 0) || (!wa.Main.Bridge.Config.Backfill.Queue.Enabled && !wa.Main.Bridge.Config.Backfill.WillPaginateManually) {
+ // If the backfill queue isn't enabled, delete all messages after backfilling a batch.
+ err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
+ } else {
+ // Otherwise just delete the messages that got backfilled
+ err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
+ }
+ if err != nil {
+ zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to delete messages from database after backfill")
+ }
+}
+
+func (wa *WhatsAppClient) convertHistorySyncMessages(
+ ctx context.Context,
+ portal *bridgev2.Portal,
+ portalJID types.JID,
+ messages []*waWeb.WebMessageInfo,
+ explodeOnError bool,
+) (*bridgev2.FetchMessagesResponse, error) {
+ oldestTS := messages[len(messages)-1].GetMessageTimestamp()
+ newestTS := messages[0].GetMessageTimestamp()
+ convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages))
var mediaRequests []*wadb.MediaRequest
for i, msg := range messages {
evt, err := wa.Client.ParseWebMessage(portalJID, msg)
if err != nil {
- // This should never happen because the info is already parsed once before being stored in the database
- return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err)
+ if explodeOnError {
+ // This should never happen because the info is already parsed once before being stored in the database
+ return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err)
+ }
+ zerolog.Ctx(ctx).Warn().Err(err).
+ Int("msg_index", i).
+ Str("msg_id", msg.GetKey().GetID()).
+ Uint64("msg_time_seconds", msg.GetMessageTimestamp()).
+ Msg("Dropping historical message due to parse error")
+ continue
+ }
+ if !explodeOnError {
+ msgType := getMessageType(evt.Message)
+ if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
+ continue
+ }
}
- var mediaReq *wadb.MediaRequest
isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
- convertedMessages[i], mediaReq = wa.convertHistorySyncMessage(
- ctx, params.Portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
+ converted, mediaReq := wa.convertHistorySyncMessage(
+ ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
)
+ convertedMessages = append(convertedMessages, converted)
if mediaReq != nil {
mediaRequests = append(mediaRequests, mediaReq)
}
@@ -509,24 +597,10 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
return &bridgev2.FetchMessagesResponse{
Messages: convertedMessages,
Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)),
- HasMore: hasMore,
- Forward: endTime == nil,
- MarkRead: markRead,
- // TODO set remaining or total count
CompleteCallback: func() {
// TODO this only deletes after backfilling. If there's no need for backfill after a relogin,
// the messages will be stuck in the database
- var err error
- if !wa.Main.Bridge.Config.Backfill.Queue.Enabled && !wa.Main.Bridge.Config.Backfill.WillPaginateManually {
- // If the backfill queue isn't enabled, delete all messages after backfilling a batch.
- err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
- } else {
- // Otherwise just delete the messages that got backfilled
- err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
- }
- if err != nil {
- zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to delete messages from database after backfill")
- }
+ wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS)
if len(mediaRequests) > 0 {
go func(ctx context.Context) {
for _, req := range mediaRequests {
@@ -544,6 +618,94 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
}, nil
}
+func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, params bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) {
+ if params.AnchorMessage == nil {
+ return nil, fmt.Errorf("anchor message is required to fetch messages from phone")
+ }
+ parsed, err := waid.ParseMessageID(params.AnchorMessage.ID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse anchor message ID: %w", err)
+ }
+
+ msgID := wa.Client.GenerateMessageID()
+ reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{
+ MessageSource: types.MessageSource{
+ Chat: parsed.Chat,
+ Sender: parsed.Sender,
+ IsFromMe: parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.Device.GetLID().ToNonAD(),
+ IsGroup: parsed.Chat.Server == types.GroupServer,
+ },
+ ID: parsed.ID,
+ Timestamp: params.AnchorMessage.Timestamp,
+ }, 50)
+ zerolog.Ctx(ctx).Debug().
+ Str("request_msg_id", msgID).
+ Any("anchor_msg_parsed", parsed).
+ Any("request_data", reqData).
+ Msg("Sending history sync request")
+ _, err = wa.Client.SendMessage(ctx, wa.JID.ToNonAD(), reqData, whatsmeow.SendRequestExtra{
+ ID: msgID,
+ Peer: true,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to send history sync request: %w", err)
+ }
+ return &bridgev2.FetchMessagesResponse{
+ HasMore: true,
+ Pending: true,
+ }, nil
+}
+
+func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *waHistorySync.HistorySync) {
+ if len(blob.GetConversations()) > 1 {
+ zerolog.Ctx(ctx).Warn().
+ Int("conversation_count", len(blob.GetConversations())).
+ Msg("Received on-demand history sync with multiple conversations")
+ }
+ for _, conv := range blob.GetConversations() {
+ portalJID, err := types.ParseJID(conv.GetID())
+ if err != nil {
+ zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID")
+ continue
+ }
+ portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(portalJID))
+ if err != nil {
+ zerolog.Ctx(ctx).Err(err).Stringer("portal_jid", portalJID).Msg("Failed to get portal for on-demand history sync")
+ continue
+ }
+ ctx := zerolog.Ctx(ctx).With().
+ Str("portal_id", string(portal.ID)).
+ Str("portal_receiver", string(portal.Receiver)).
+ Stringer("portal_mxid", portal.MXID).
+ Logger().WithContext(ctx)
+ portal.HandleRemoteBackfill(ctx, wa.UserLogin, &simplevent.Backfill{
+ EventMeta: simplevent.EventMeta{
+ Type: bridgev2.RemoteEventBackfill,
+ PortalKey: portal.PortalKey,
+ },
+ GetDataFunc: func(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.FetchMessagesResponse, error) {
+ if len(conv.GetMessages()) == 0 {
+ return &bridgev2.FetchMessagesResponse{}, nil
+ }
+ messages := make([]*waWeb.WebMessageInfo, len(conv.GetMessages()))
+ for i, rawMsg := range conv.GetMessages() {
+ messages[i] = rawMsg.Message
+ }
+ zerolog.Ctx(ctx).Debug().
+ Int("message_count", len(messages)).
+ Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()).
+ Msg("Converting messages to bridge from on-demand history sync")
+ resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false)
+ if err != nil {
+ return nil, err
+ }
+ resp.HasMore = conv.GetEndOfHistoryTransferType() == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
+ return resp, nil
+ },
+ })
+ }
+}
+
func (wa *WhatsAppClient) convertHistorySyncMessage(
ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction,
) (*bridgev2.BackfillMessage, *wadb.MediaRequest) {
diff --git a/pkg/connector/config.go b/pkg/connector/config.go
index 6a097c2..2445647 100644
--- a/pkg/connector/config.go
+++ b/pkg/connector/config.go
@@ -70,6 +70,8 @@ type Config struct {
RequestLocalTime int `yaml:"request_local_time"`
MaxAsyncHandle int64 `yaml:"max_async_handle"`
} `yaml:"media_requests"`
+
+ BackwardsOnDemand bool `yaml:"backwards_on_demand"`
} `yaml:"history_sync"`
displaynameTemplate *template.Template `yaml:"-"`
@@ -134,6 +136,7 @@ func upgradeConfig(helper up.Helper) {
helper.Copy(up.Str, "history_sync", "media_requests", "request_method")
helper.Copy(up.Int, "history_sync", "media_requests", "request_local_time")
helper.Copy(up.Int, "history_sync", "media_requests", "max_async_handle")
+ helper.Copy(up.Bool, "history_sync", "backwards_on_demand")
}
type DisplaynameParams struct {
diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml
index e441f67..564f25e 100644
--- a/pkg/connector/example-config.yaml
+++ b/pkg/connector/example-config.yaml
@@ -121,3 +121,6 @@ history_sync:
request_local_time: 120
# Maximum number of media request responses to handle in parallel per user.
max_async_handle: 2
+ # Use on-demand history sync requests for fetching older messages?
+ # This only applies when using the backfill queue, never for forward backfills.
+ backwards_on_demand: false
From b1c89119437dd963844894a607ed8ac4b75e9d25 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 3 Apr 2026 18:16:20 +0300
Subject: [PATCH 05/94] .github: add checklist to bug report template
---
.github/ISSUE_TEMPLATE/bug.md | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
index 3703df9..c10630f 100644
--- a/.github/ISSUE_TEMPLATE/bug.md
+++ b/.github/ISSUE_TEMPLATE/bug.md
@@ -7,10 +7,11 @@ type: Bug
---
-
-It's always best to ask in the Matrix room first, especially if you aren't sure
-what details are needed. Issues with insufficient detail will likely just be
-ignored or closed immediately.
--->
+### Checklist
+
+
+
+* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
+* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
From 2fc79dba1a2f771d8034bda7f965ab197695c8c7 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 8 Apr 2026 19:59:07 +0300
Subject: [PATCH 06/94] dependencies: update mautrix-go
---
go.mod | 4 ++--
go.sum | 8 ++++----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/go.mod b/go.mod
index a28f7c7..80eacc1 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ tool go.mau.fi/util/cmd/maubuild
require (
github.com/lib/pq v1.12.0
github.com/rs/zerolog v1.35.0
- go.mau.fi/util v0.9.7
+ go.mau.fi/util v0.9.8-0.20260406161447-0300c476893a
go.mau.fi/webp v0.2.0
go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4
golang.org/x/image v0.37.0
@@ -17,7 +17,7 @@ require (
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.26.5-0.20260331163037-18917f3bdc14
+ maunium.net/go/mautrix v0.26.5-0.20260408131844-9db6af36a393
)
require (
diff --git a/go.sum b/go.sum
index ff1245a..d6183fc 100644
--- a/go.sum
+++ b/go.sum
@@ -71,8 +71,8 @@ github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
-go.mau.fi/util v0.9.7 h1:AWGNbJfz1zRcQOKeOEYhKUG2fT+/26Gy6kyqcH8tnBg=
-go.mau.fi/util v0.9.7/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE=
+go.mau.fi/util v0.9.8-0.20260406161447-0300c476893a h1:OQQF3rTJH10l6+dcP0OKnYbNDMBTGoIZZINNJm8QBG8=
+go.mau.fi/util v0.9.8-0.20260406161447-0300c476893a/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4 h1:E4A6eca9vMJQctC9DIfzUIg27TrJ8IrDHgkJwJ8WPUQ=
@@ -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.26.5-0.20260331163037-18917f3bdc14 h1:y+4gtqKBMTtcVUiAeWJnvp88JLo/h3myQPsz1rZfNOY=
-maunium.net/go/mautrix v0.26.5-0.20260331163037-18917f3bdc14/go.mod h1:RUSMBPky3jhXB7Ux+AptfkEvFlJ4ajZKCYiXI8YzxVE=
+maunium.net/go/mautrix v0.26.5-0.20260408131844-9db6af36a393 h1:mvoL0cW9nbMO6sJjyO1JZvK5C4y1wr7+JjyyU70Lhuw=
+maunium.net/go/mautrix v0.26.5-0.20260408131844-9db6af36a393/go.mod h1:MX4DQLiBe0c7sI/wizruqdxHinSOWs42/DYsP9GH7Q4=
From 8b16f9d470a4d783c40645176fa5da96ede94678 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 10 Apr 2026 17:34:05 +0300
Subject: [PATCH 07/94] backfill: log when deleting messages
---
pkg/connector/backfill.go | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go
index f33351f..8a29311 100644
--- a/pkg/connector/backfill.go
+++ b/pkg/connector/backfill.go
@@ -549,7 +549,17 @@ func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJ
err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
}
if err != nil {
- zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to delete messages from database after backfill")
+ zerolog.Ctx(ctx).Warn().Err(err).
+ Stringer("portal_jid", portalJID).
+ Uint64("newest_ts", newestTS).
+ Uint64("oldest_ts", oldestTS).
+ Msg("Failed to delete messages from database after backfill")
+ } else {
+ zerolog.Ctx(ctx).Debug().
+ Stringer("portal_jid", portalJID).
+ Uint64("newest_ts", newestTS).
+ Uint64("oldest_ts", oldestTS).
+ Msg("Deleted history sync messages from database")
}
}
From 520d7b9f266ff42bb8c7e944878ddbe830f6b694 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 10 Apr 2026 15:00:46 +0300
Subject: [PATCH 08/94] backfill: use new slow fetch flags
---
go.mod | 2 +-
go.sum | 4 ++--
pkg/connector/backfill.go | 11 +++++++++--
3 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/go.mod b/go.mod
index 80eacc1..15383af 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.26.5-0.20260408131844-9db6af36a393
+ maunium.net/go/mautrix v0.26.5-0.20260410170402-f76d1a0ce631
)
require (
diff --git a/go.sum b/go.sum
index d6183fc..c25697d 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.26.5-0.20260408131844-9db6af36a393 h1:mvoL0cW9nbMO6sJjyO1JZvK5C4y1wr7+JjyyU70Lhuw=
-maunium.net/go/mautrix v0.26.5-0.20260408131844-9db6af36a393/go.mod h1:MX4DQLiBe0c7sI/wizruqdxHinSOWs42/DYsP9GH7Q4=
+maunium.net/go/mautrix v0.26.5-0.20260410170402-f76d1a0ce631 h1:rx0v5XOqjBIlQ5oyD2tgRu8C2pHGJnkNyFoZcg4YbeU=
+maunium.net/go/mautrix v0.26.5-0.20260410170402-f76d1a0ce631/go.mod h1:MX4DQLiBe0c7sI/wizruqdxHinSOWs42/DYsP9GH7Q4=
diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go
index 8a29311..8ce0cc5 100644
--- a/pkg/connector/backfill.go
+++ b/pkg/connector/backfill.go
@@ -509,7 +509,14 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
if err != nil {
return nil, fmt.Errorf("failed to load messages from database: %w", err)
} else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) {
- if hasMore {
+ wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0)
+ if hasMore && !params.AllowSlowFetch {
+ return &bridgev2.FetchMessagesResponse{
+ MoreRequiresSlowFetch: true,
+ HasMore: true,
+ Forward: params.Forward,
+ }, nil
+ } else if hasMore {
return wa.fetchMessagesFromPhone(ctx, params)
}
return &bridgev2.FetchMessagesResponse{
@@ -541,7 +548,7 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) {
var err error
- if (newestTS == 0 && oldestTS == 0) || (!wa.Main.Bridge.Config.Backfill.Queue.Enabled && !wa.Main.Bridge.Config.Backfill.WillPaginateManually) {
+ if (newestTS == 0 && oldestTS == 0) || !wa.Main.Bridge.Config.Backfill.Queue.AnyEnabled() {
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
} else {
From e218f9c62975035c1d325ec22b6428eca5bec2f2 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 10 Apr 2026 20:12:51 +0300
Subject: [PATCH 09/94] backfill: log number of rows when deleting messages
---
pkg/connector/backfill.go | 6 ++++--
pkg/connector/wadb/message.go | 18 ++++++++++++------
2 files changed, 16 insertions(+), 8 deletions(-)
diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go
index 8ce0cc5..e20d717 100644
--- a/pkg/connector/backfill.go
+++ b/pkg/connector/backfill.go
@@ -548,12 +548,13 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) {
var err error
+ var rows int64
if (newestTS == 0 && oldestTS == 0) || !wa.Main.Bridge.Config.Backfill.Queue.AnyEnabled() {
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
- err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
+ rows, err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
} else {
// Otherwise just delete the messages that got backfilled
- err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
+ rows, err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
}
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).
@@ -566,6 +567,7 @@ func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJ
Stringer("portal_jid", portalJID).
Uint64("newest_ts", newestTS).
Uint64("oldest_ts", oldestTS).
+ Int64("rows_affected", rows).
Msg("Deleted history sync messages from database")
}
}
diff --git a/pkg/connector/wadb/message.go b/pkg/connector/wadb/message.go
index f2450c1..4b16002 100644
--- a/pkg/connector/wadb/message.go
+++ b/pkg/connector/wadb/message.go
@@ -116,9 +116,12 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo
AsList()
}
-func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) error {
- _, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after)
- return err
+func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) (int64, error) {
+ res, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after)
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
}
func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLoginID) error {
@@ -126,9 +129,12 @@ func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLog
return err
}
-func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) error {
- _, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID)
- return err
+func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (int64, error) {
+ res, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID)
+ if err != nil {
+ return 0, err
+ }
+ return res.RowsAffected()
}
func (mq *MessageQuery) ConversationHasMessages(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (exists bool, err error) {
From 6510ff1ffd98479d472f0b1b43d3f08e89967b4f Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Mon, 13 Apr 2026 12:34:32 +0300
Subject: [PATCH 10/94] backfill: do initial backfill even if only bootstrap is
received
---
pkg/connector/backfill.go | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go
index e20d717..90c1316 100644
--- a/pkg/connector/backfill.go
+++ b/pkg/connector/backfill.go
@@ -138,7 +138,7 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
return
}
err = wa.Main.DB.DoTxn(ctx, nil, func(ctx context.Context) (innerErr error) {
- resetTimer, innerErr = wa.handleWAHistorySync(ctx, evt, blob, true)
+ innerErr = wa.handleWAHistorySync(ctx, evt, blob, true)
if innerErr != nil {
return
}
@@ -151,6 +151,9 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
if err != nil {
log.Err(err).Msg("Failed to store history sync notification data")
} else {
+ resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP ||
+ blob.GetSyncType() == waHistorySync.HistorySync_RECENT ||
+ blob.GetSyncType() == waHistorySync.HistorySync_FULL
err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle())
if err != nil {
log.Err(err).Msg("Failed to delete history sync blob from server")
@@ -166,9 +169,9 @@ func (wa *WhatsAppClient) handleWAHistorySync(
notif *waE2E.HistorySyncNotification,
evt *waHistorySync.HistorySync,
stopOnError bool,
-) (bool, error) {
+) error {
if evt == nil || evt.SyncType == nil {
- return false, nil
+ return nil
}
log := wa.UserLogin.Log.With().
Str("action", "store history sync").
@@ -193,7 +196,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Int("recent_sticker_count", len(evt.GetRecentStickers())).
Int("past_participant_count", len(evt.GetPastParticipants())).
Msg("Ignoring history sync")
- return false, nil
+ return nil
}
log.Info().
Int("conversation_count", len(evt.GetConversations())).
@@ -309,7 +312,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime))
if err != nil {
if stopOnError {
- return false, fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
+ return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
}
log.Err(err).Msg("Failed to save conversation metadata")
continue
@@ -317,7 +320,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages)
if err != nil {
if stopOnError {
- return false, fmt.Errorf("failed to save messages in %s: %w", jid, err)
+ return fmt.Errorf("failed to save messages in %s: %w", jid, err)
}
log.Err(err).Msg("Failed to save messages")
failedToSaveTotal += len(messages)
@@ -327,7 +330,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
err = wa.Main.Bridge.DB.BackfillTask.MarkNotDone(ctx, wa.makeWAPortalKey(jid), wa.UserLogin.ID)
if err != nil {
if stopOnError {
- return false, fmt.Errorf("failed to mark backfill task as not done for %s: %w", jid, err)
+ return fmt.Errorf("failed to mark backfill task as not done for %s: %w", jid, err)
}
log.Err(err).Msg("Failed to mark backfill task as not done")
}
@@ -339,9 +342,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Int("total_message_count", totalMessageCount).
Dur("duration", time.Since(start)).
Msg("Finished storing history sync")
- resetTimer := evt.GetSyncType() == waHistorySync.HistorySync_RECENT ||
- evt.GetSyncType() == waHistorySync.HistorySync_FULL
- return resetTimer, nil
+ return nil
}
func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
From 3dc3c1a603e64a089f617537406a6fcbe74fb4fe Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Tue, 14 Apr 2026 14:38:18 +0300
Subject: [PATCH 11/94] dependencies: update
---
go.mod | 6 +++---
go.sum | 8 ++++----
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/go.mod b/go.mod
index 15383af..dfd3692 100644
--- a/go.mod
+++ b/go.mod
@@ -2,7 +2,7 @@ module go.mau.fi/mautrix-whatsapp
go 1.25.0
-toolchain go1.26.1
+toolchain go1.26.2
tool go.mau.fi/util/cmd/maubuild
@@ -11,13 +11,13 @@ require (
github.com/rs/zerolog v1.35.0
go.mau.fi/util v0.9.8-0.20260406161447-0300c476893a
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4
+ go.mau.fi/whatsmeow v0.0.0-20260410162419-b95d92207080
golang.org/x/image v0.37.0
golang.org/x/net v0.52.0
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.26.5-0.20260410170402-f76d1a0ce631
+ maunium.net/go/mautrix v0.26.5-0.20260413182302-f3fab8d38470
)
require (
diff --git a/go.sum b/go.sum
index c25697d..a7d7ef6 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,8 @@ go.mau.fi/util v0.9.8-0.20260406161447-0300c476893a h1:OQQF3rTJH10l6+dcP0OKnYbND
go.mau.fi/util v0.9.8-0.20260406161447-0300c476893a/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4 h1:E4A6eca9vMJQctC9DIfzUIg27TrJ8IrDHgkJwJ8WPUQ=
-go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM=
+go.mau.fi/whatsmeow v0.0.0-20260410162419-b95d92207080 h1:j7D8kKa7A1n9kUTmVqHfwHNtoSkgM7FsiSyko/Tye5o=
+go.mau.fi/whatsmeow v0.0.0-20260410162419-b95d92207080/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
@@ -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.26.5-0.20260410170402-f76d1a0ce631 h1:rx0v5XOqjBIlQ5oyD2tgRu8C2pHGJnkNyFoZcg4YbeU=
-maunium.net/go/mautrix v0.26.5-0.20260410170402-f76d1a0ce631/go.mod h1:MX4DQLiBe0c7sI/wizruqdxHinSOWs42/DYsP9GH7Q4=
+maunium.net/go/mautrix v0.26.5-0.20260413182302-f3fab8d38470 h1:vehV8Ev2TzpV5DH9ToCxt43svkXRKcn/kJaZ4mNvRFQ=
+maunium.net/go/mautrix v0.26.5-0.20260413182302-f3fab8d38470/go.mod h1:MX4DQLiBe0c7sI/wizruqdxHinSOWs42/DYsP9GH7Q4=
From 32ad31902ef24730e5dcf29a0f8f6fab2414c67f Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 16 Apr 2026 16:44:29 +0300
Subject: [PATCH 12/94] Bump version to v26.04
---
CHANGELOG.md | 7 +++++
cmd/mautrix-whatsapp/main.go | 2 +-
go.mod | 26 +++++++++---------
go.sum | 52 ++++++++++++++++++------------------
4 files changed, 47 insertions(+), 40 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3cefd6d..892be2e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# v26.04
+
+* Added support for @room mentions in both directions.
+* Changed initial backfill to happen even if WhatsApp doesn't send full history.
+* Fixed panic when handling updates to unknown polls from WhatsApp.
+* Fixed some background loops not stopping when a user is logged out.
+
# v26.03
* Added option to save outgoing messages in the database to allow encryption
diff --git a/cmd/mautrix-whatsapp/main.go b/cmd/mautrix-whatsapp/main.go
index e64f02f..b97bc7f 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.03",
+ Version: "26.04",
SemCalVer: true,
Connector: &connector.WhatsAppConnector{},
}
diff --git a/go.mod b/go.mod
index dfd3692..9aaca46 100644
--- a/go.mod
+++ b/go.mod
@@ -7,17 +7,17 @@ toolchain go1.26.2
tool go.mau.fi/util/cmd/maubuild
require (
- github.com/lib/pq v1.12.0
+ github.com/lib/pq v1.12.3
github.com/rs/zerolog v1.35.0
- go.mau.fi/util v0.9.8-0.20260406161447-0300c476893a
+ go.mau.fi/util v0.9.8
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260410162419-b95d92207080
- golang.org/x/image v0.37.0
- golang.org/x/net v0.52.0
+ go.mau.fi/whatsmeow v0.0.0-20260416104156-3ff20cd3462a
+ golang.org/x/image v0.39.0
+ golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.26.5-0.20260413182302-f3fab8d38470
+ maunium.net/go/mautrix v0.27.0
)
require (
@@ -30,8 +30,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.37 // indirect
- github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect
+ github.com/mattn/go-sqlite3 v1.14.42 // indirect
+ github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // 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/yuin/goldmark v1.8.2 // indirect
go.mau.fi/libsignal v0.2.1 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
- golang.org/x/crypto v0.49.0 // indirect
- golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
- golang.org/x/mod v0.34.0 // indirect
- golang.org/x/sys v0.42.0 // indirect
- golang.org/x/text v0.35.0 // indirect
+ golang.org/x/crypto v0.50.0 // indirect
+ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
+ golang.org/x/mod v0.35.0 // indirect
+ golang.org/x/sys v0.43.0 // indirect
+ golang.org/x/text v0.36.0 // indirect
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 a7d7ef6..dd982b9 100644
--- a/go.sum
+++ b/go.sum
@@ -28,16 +28,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo=
-github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
+github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
+github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.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.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
-github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 h1:rh2lKw/P/EqHa724vYH2+VVQ1YnW4u6EOXl0PMAovZE=
-github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
+github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
+github.com/mattn/go-sqlite3 v1.14.42/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/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=
@@ -71,31 +71,31 @@ github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
-go.mau.fi/util v0.9.8-0.20260406161447-0300c476893a h1:OQQF3rTJH10l6+dcP0OKnYbNDMBTGoIZZINNJm8QBG8=
-go.mau.fi/util v0.9.8-0.20260406161447-0300c476893a/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE=
+go.mau.fi/util v0.9.8 h1:+/jf8eM2dAT2wx9UidmaneH28r/CSCKCniCyby1qWz8=
+go.mau.fi/util v0.9.8/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260410162419-b95d92207080 h1:j7D8kKa7A1n9kUTmVqHfwHNtoSkgM7FsiSyko/Tye5o=
-go.mau.fi/whatsmeow v0.0.0-20260410162419-b95d92207080/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM=
+go.mau.fi/whatsmeow v0.0.0-20260416104156-3ff20cd3462a h1:/7erOAOkZ5d/k9bghMMQPciR0ypmOsM8wGv7bIwyyZo=
+go.mau.fi/whatsmeow v0.0.0-20260416104156-3ff20cd3462a/go.mod h1:B/y3nOUaK8BDJKvyvq6YbLh2UKTCoiA5xQ2sFwbuOWk=
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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
-golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
-golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
-golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
-golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
-golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
-golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
-golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
-golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
-golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
+golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
+golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
+golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
+golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
+golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
+golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
+golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
+golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
+golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
-golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
+golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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.26.5-0.20260413182302-f3fab8d38470 h1:vehV8Ev2TzpV5DH9ToCxt43svkXRKcn/kJaZ4mNvRFQ=
-maunium.net/go/mautrix v0.26.5-0.20260413182302-f3fab8d38470/go.mod h1:MX4DQLiBe0c7sI/wizruqdxHinSOWs42/DYsP9GH7Q4=
+maunium.net/go/mautrix v0.27.0 h1:yfEYwoIluVWkofUgbZl9gP4i5nQTF+QNsxtb+r5bKlM=
+maunium.net/go/mautrix v0.27.0/go.mod h1:7QpEQiTy6p4LHkXXaZI+N46tGYy8HMhD0JjzZAFoFWs=
From 7254783a32bb3361efc716c9571a2b33cfacd9a1 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 24 Apr 2026 12:46:48 +0300
Subject: [PATCH 13/94] backfill: add debug logs for incorrect timestamps from
phone
---
pkg/connector/backfill.go | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go
index 90c1316..b4ef181 100644
--- a/pkg/connector/backfill.go
+++ b/pkg/connector/backfill.go
@@ -246,7 +246,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
return c.Stringer("chat_jid", jid)
})
- var minTime, maxTime time.Time
+ var minTime, maxTime, firstItemTime, lastItemTime time.Time
var minTimeIndex, maxTimeIndex int
ignoredTypes := 0
@@ -262,6 +262,10 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Msg("Dropping historical message due to parse error")
continue
}
+ if firstItemTime.IsZero() {
+ firstItemTime = msgEvt.Info.Timestamp
+ }
+ lastItemTime = msgEvt.Info.Timestamp
if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) {
minTime = msgEvt.Info.Timestamp
minTimeIndex = i
@@ -294,6 +298,9 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Int("lowest_time_index", minTimeIndex).
Time("highest_time", maxTime).
Int("highest_time_index", maxTimeIndex).
+ Time("first_item_time", firstItemTime).
+ Time("last_item_time", lastItemTime).
+ Bool("highest_time_mismatch", firstItemTime != maxTime).
Dict("metadata", zerolog.Dict().
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
From 8af9a2f4977333d928b73bb9a85aaffc41ccc829 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Tue, 28 Apr 2026 18:16:17 +0300
Subject: [PATCH 14/94] msgconv/sticker: parse metadata from received stickers
---
go.mod | 8 +-
go.sum | 12 +-
pkg/connector/directmedia.go | 2 +-
pkg/msgconv/wa-media.go | 102 +-------------
pkg/msgconv/wa-sticker.go | 266 +++++++++++++++++++++++++++++++++++
5 files changed, 282 insertions(+), 108 deletions(-)
create mode 100644 pkg/msgconv/wa-sticker.go
diff --git a/go.mod b/go.mod
index 9aaca46..3054e5a 100644
--- a/go.mod
+++ b/go.mod
@@ -9,15 +9,16 @@ tool go.mau.fi/util/cmd/maubuild
require (
github.com/lib/pq v1.12.3
github.com/rs/zerolog v1.35.0
- go.mau.fi/util v0.9.8
+ github.com/tidwall/gjson v1.18.0
+ go.mau.fi/util v0.9.9-0.20260428124215-c47a7212562e
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260416104156-3ff20cd3462a
+ go.mau.fi/whatsmeow v0.0.0-20260427122815-7514259253a7
golang.org/x/image v0.39.0
golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.27.0
+ maunium.net/go/mautrix v0.27.1-0.20260428110059-49a05bf06436
)
require (
@@ -35,7 +36,6 @@ require (
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
- github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
diff --git a/go.sum b/go.sum
index dd982b9..ebe18fa 100644
--- a/go.sum
+++ b/go.sum
@@ -71,12 +71,12 @@ github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
-go.mau.fi/util v0.9.8 h1:+/jf8eM2dAT2wx9UidmaneH28r/CSCKCniCyby1qWz8=
-go.mau.fi/util v0.9.8/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0=
+go.mau.fi/util v0.9.9-0.20260428124215-c47a7212562e h1:o0O9sLa4CeZbxbgoSqavwaORrt9BB+trOLKBSoGzJ3Q=
+go.mau.fi/util v0.9.9-0.20260428124215-c47a7212562e/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260416104156-3ff20cd3462a h1:/7erOAOkZ5d/k9bghMMQPciR0ypmOsM8wGv7bIwyyZo=
-go.mau.fi/whatsmeow v0.0.0-20260416104156-3ff20cd3462a/go.mod h1:B/y3nOUaK8BDJKvyvq6YbLh2UKTCoiA5xQ2sFwbuOWk=
+go.mau.fi/whatsmeow v0.0.0-20260427122815-7514259253a7 h1:jEOI4I7kU+MYUNI1L94rhYXhUg8N9+YUNHVY525aYTc=
+go.mau.fi/whatsmeow v0.0.0-20260427122815-7514259253a7/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
@@ -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.27.0 h1:yfEYwoIluVWkofUgbZl9gP4i5nQTF+QNsxtb+r5bKlM=
-maunium.net/go/mautrix v0.27.0/go.mod h1:7QpEQiTy6p4LHkXXaZI+N46tGYy8HMhD0JjzZAFoFWs=
+maunium.net/go/mautrix v0.27.1-0.20260428110059-49a05bf06436 h1:vga9ypiOLJmGguxq4D1aquDPFihOuD99EGPEwva12UI=
+maunium.net/go/mautrix v0.27.1-0.20260428110059-49a05bf06436/go.mod h1:4fZ0M0xB5ZtueQI65RilX28J/3794BeK+LaCg4U61Jk=
diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go
index a4a3d73..10cdbbb 100644
--- a/pkg/connector/directmedia.go
+++ b/pkg/connector/directmedia.go
@@ -203,7 +203,7 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err)
} else if zipData, err := io.ReadAll(f); err != nil {
return nil, fmt.Errorf("failed to read sticker zip: %w", err)
- } else if data, err := msgconv.ExtractAnimatedSticker(zipData); err != nil {
+ } else if data, _, err := msgconv.ExtractAnimatedSticker(zipData); err != nil {
return nil, fmt.Errorf("failed to extract animated sticker: %w %x", err, zipData)
} else if _, err := f.WriteAt(data, 0); err != nil {
return nil, fmt.Errorf("failed to write animated sticker to file: %w", err)
diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go
index 9a1ceb3..d1b5332 100644
--- a/pkg/msgconv/wa-media.go
+++ b/pkg/msgconv/wa-media.go
@@ -17,8 +17,6 @@
package msgconv
import (
- "archive/zip"
- "bytes"
"context"
"encoding/json"
"errors"
@@ -26,15 +24,11 @@ import (
"io"
"net/http"
"os"
- "path/filepath"
- "strconv"
"strings"
"github.com/rs/zerolog"
"go.mau.fi/util/exmime"
"go.mau.fi/util/exslices"
- "go.mau.fi/util/lottie"
- "go.mau.fi/util/random"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types"
@@ -198,7 +192,9 @@ type PreparedMedia struct {
}
func (pm *PreparedMedia) FillFileName() *PreparedMedia {
- if pm.FileName == "" {
+ if pm.Type == event.EventSticker {
+ pm.FileName = ""
+ } else if pm.FileName == "" {
pm.FileName = strings.TrimPrefix(string(pm.MsgType), "m.") + exmime.ExtensionFromMimetype(pm.Info.MimeType)
}
return pm
@@ -287,9 +283,6 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
case *waE2E.StickerMessage:
data.Type = event.EventSticker
data.FileName = "sticker" + exmime.ExtensionFromMimetype(msg.GetMimetype())
- if msg.GetMimetype() == "application/was" && data.FileName == "sticker" {
- data.FileName = "sticker.json"
- }
if data.Info.Width == data.Info.Height {
data.Info.Width = WhatsAppStickerSize
data.Info.Height = WhatsAppStickerSize
@@ -397,6 +390,8 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
if err != nil {
return err
}
+ } else if part.Type == event.EventSticker && part.Info.MimeType == "image/webp" {
+ mc.fillWebPStickerInfo(ctx, part, data)
}
if part.Info.MimeType == "" {
part.Info.MimeType = http.DetectContentType(data)
@@ -425,68 +420,6 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
return nil
}
-func (mc *MessageConverter) extractAnimatedSticker(fileInfo *PreparedMedia, data []byte) ([]byte, error) {
- data, err := ExtractAnimatedSticker(data)
- if err != nil {
- return nil, err
- }
- fileInfo.Info.MimeType = "video/lottie+json"
- fileInfo.FileName = "sticker.json"
- return data, nil
-}
-
-func (mc *MessageConverter) convertAnimatedSticker(ctx context.Context, fileInfo *PreparedMedia, data []byte) ([]byte, []byte, *event.FileInfo, error) {
- data, err := mc.extractAnimatedSticker(fileInfo, data)
- if err != nil {
- return nil, nil, nil, err
- }
- c := mc.AnimatedStickerConfig
- if c.Target == "disable" {
- return data, nil, nil, nil
- } else if !lottie.Supported() {
- zerolog.Ctx(ctx).Warn().Msg("Animated sticker conversion is enabled, but lottieconverter is not installed")
- return data, nil, nil, nil
- }
- input := bytes.NewReader(data)
- fileInfo.Info.MimeType = "image/" + c.Target
- fileInfo.FileName = "sticker." + c.Target
- switch c.Target {
- case "png":
- var output bytes.Buffer
- err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, "1")
- return output.Bytes(), nil, nil, err
- case "gif":
- var output bytes.Buffer
- err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS))
- return output.Bytes(), nil, nil, err
- case "webm", "webp":
- tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("mautrix-whatsapp-lottieconverter-%s.%s", random.String(10), c.Target))
- defer func() {
- _ = os.Remove(tmpFile)
- }()
- thumbnailData, err := lottie.FFmpegConvert(ctx, input, tmpFile, c.Args.Width, c.Args.Height, c.Args.FPS)
- if err != nil {
- return nil, nil, nil, err
- }
- data, err = os.ReadFile(tmpFile)
- if err != nil {
- return nil, nil, nil, fmt.Errorf("failed to read converted file: %w", err)
- }
- var thumbnailInfo *event.FileInfo
- if thumbnailData != nil {
- thumbnailInfo = &event.FileInfo{
- MimeType: "image/png",
- Width: c.Args.Width,
- Height: c.Args.Height,
- Size: len(thumbnailData),
- }
- }
- return data, thumbnailData, thumbnailInfo, nil
- default:
- return nil, nil, nil, fmt.Errorf("unsupported target format %s", c.Target)
- }
-}
-
func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *PreparedMedia, keys *FailedMediaKeys, err error) *bridgev2.ConvertedMessagePart {
logLevel := zerolog.ErrorLevel
var extra map[string]any
@@ -531,28 +464,3 @@ func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *Pre
}
return part
}
-
-func ExtractAnimatedSticker(data []byte) ([]byte, error) {
- zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
- if err != nil {
- return nil, fmt.Errorf("failed to read sticker zip: %w", err)
- }
- animationFile, err := zipReader.Open("animation/animation.json")
- if err != nil {
- return nil, fmt.Errorf("failed to open animation.json: %w", err)
- }
- animationFileInfo, err := animationFile.Stat()
- if err != nil {
- _ = animationFile.Close()
- return nil, fmt.Errorf("failed to stat animation.json: %w", err)
- } else if animationFileInfo.Size() > uploadFileThreshold {
- _ = animationFile.Close()
- return nil, fmt.Errorf("animation.json is too large (%.2f MiB)", float64(animationFileInfo.Size())/1024/1024)
- }
- data, err = io.ReadAll(animationFile)
- _ = animationFile.Close()
- if err != nil {
- return nil, fmt.Errorf("failed to read animation.json: %w", err)
- }
- return data, nil
-}
diff --git a/pkg/msgconv/wa-sticker.go b/pkg/msgconv/wa-sticker.go
new file mode 100644
index 0000000..fef111a
--- /dev/null
+++ b/pkg/msgconv/wa-sticker.go
@@ -0,0 +1,266 @@
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
+// Copyright (C) 2026 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package msgconv
+
+import (
+ "archive/zip"
+ "bytes"
+ "context"
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/rs/zerolog"
+ "github.com/tidwall/gjson"
+ "go.mau.fi/util/exstrings"
+ "go.mau.fi/util/lottie"
+ "go.mau.fi/util/random"
+ "maunium.net/go/mautrix/event"
+)
+
+type StickerMetadata struct {
+ StickerPackID string `json:"sticker-pack-id"`
+ AccessibilityText string `json:"accessibility-text"`
+ Emojis []string `json:"emojis"`
+ IsFirstPartySticker int `json:"is-first-party-sticker"`
+}
+
+func (sm *StickerMetadata) ToMatrix(content *event.MessageEventContent) {
+ if sm == nil {
+ return
+ }
+ if sm.StickerPackID != "" {
+ content.Info.BridgedSticker = &event.BridgedSticker{
+ Network: StickerSourceID,
+ PackURL: StickerPackURLPrefix + sm.StickerPackID,
+ }
+ if len(sm.Emojis) > 0 {
+ content.Info.BridgedSticker.Emoji = sm.Emojis[0]
+ }
+ }
+ if sm.AccessibilityText != "" {
+ content.Body = sm.AccessibilityText
+ } else if len(sm.Emojis) > 0 {
+ content.Body = strings.Join(sm.Emojis, " ")
+ }
+}
+
+const StickerSourceID = "whatsapp"
+const StickerPackURLPrefix = "https://wa.me/stickerpack/"
+
+func ExtractAnimatedSticker(data []byte) ([]byte, *StickerMetadata, error) {
+ zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to read sticker zip: %w", err)
+ }
+ animationFile, err := zipReader.Open("animation/animation.json")
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to open animation.json: %w", err)
+ }
+ animationFileInfo, err := animationFile.Stat()
+ if err != nil {
+ _ = animationFile.Close()
+ return nil, nil, fmt.Errorf("failed to stat animation.json: %w", err)
+ } else if animationFileInfo.Size() > uploadFileThreshold {
+ _ = animationFile.Close()
+ return nil, nil, fmt.Errorf("animation.json is too large (%.2f MiB)", float64(animationFileInfo.Size())/1024/1024)
+ }
+ data, err = io.ReadAll(animationFile)
+ _ = animationFile.Close()
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to read animation.json: %w", err)
+ }
+ var meta StickerMetadata
+ metaFile, err := zipReader.Open("animation/animation.json.overridden_metadata")
+ if err == nil {
+ _ = json.NewDecoder(metaFile).Decode(&meta)
+ _ = metaFile.Close()
+ }
+ if meta.StickerPackID == "" {
+ res := gjson.GetBytes(data, "metadata.customProps")
+ if res.IsObject() {
+ _ = json.Unmarshal(exstrings.UnsafeBytes(res.Raw), &meta)
+ }
+ }
+ return data, &meta, nil
+}
+
+func (mc *MessageConverter) extractAnimatedSticker(fileInfo *PreparedMedia, data []byte) ([]byte, error) {
+ data, meta, err := ExtractAnimatedSticker(data)
+ if err != nil {
+ return nil, err
+ }
+ meta.ToMatrix(fileInfo.MessageEventContent)
+ fileInfo.Info.MimeType = "video/lottie+json"
+ fileInfo.FileName = "sticker.json"
+ return data, nil
+}
+
+func (mc *MessageConverter) convertAnimatedSticker(ctx context.Context, fileInfo *PreparedMedia, data []byte) ([]byte, []byte, *event.FileInfo, error) {
+ data, err := mc.extractAnimatedSticker(fileInfo, data)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ c := mc.AnimatedStickerConfig
+ if c.Target == "disable" {
+ return data, nil, nil, nil
+ } else if !lottie.Supported() {
+ zerolog.Ctx(ctx).Warn().Msg("Animated sticker conversion is enabled, but lottieconverter is not installed")
+ return data, nil, nil, nil
+ }
+ input := bytes.NewReader(data)
+ fileInfo.Info.MimeType = "image/" + c.Target
+ fileInfo.FileName = "sticker." + c.Target
+ switch c.Target {
+ case "png":
+ var output bytes.Buffer
+ err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, "1")
+ return output.Bytes(), nil, nil, err
+ case "gif":
+ var output bytes.Buffer
+ err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS))
+ return output.Bytes(), nil, nil, err
+ case "webm", "webp":
+ tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("mautrix-whatsapp-lottieconverter-%s.%s", random.String(10), c.Target))
+ defer func() {
+ _ = os.Remove(tmpFile)
+ }()
+ thumbnailData, err := lottie.FFmpegConvert(ctx, input, tmpFile, c.Args.Width, c.Args.Height, c.Args.FPS)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ data, err = os.ReadFile(tmpFile)
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("failed to read converted file: %w", err)
+ }
+ var thumbnailInfo *event.FileInfo
+ if thumbnailData != nil {
+ thumbnailInfo = &event.FileInfo{
+ MimeType: "image/png",
+ Width: c.Args.Width,
+ Height: c.Args.Height,
+ Size: len(thumbnailData),
+ }
+ }
+ return data, thumbnailData, thumbnailInfo, nil
+ default:
+ return nil, nil, nil, fmt.Errorf("unsupported target format %s", c.Target)
+ }
+}
+
+func (mc *MessageConverter) fillWebPStickerInfo(ctx context.Context, fileInfo *PreparedMedia, data []byte) {
+ meta, err := extractWebPStickerMetadata(data)
+ if err != nil {
+ zerolog.Ctx(ctx).Debug().Err(err).Msg("Failed to extract webp sticker metadata")
+ return
+ }
+ meta.ToMatrix(fileInfo.MessageEventContent)
+}
+
+// stickerMetadataEXIFTag is the custom EXIF tag WhatsApp uses to embed
+// sticker pack metadata as a JSON object inside non-animated webp stickers.
+const stickerMetadataEXIFTag = 0x5741
+
+// extractWebPStickerMetadata parses the WhatsApp sticker pack metadata JSON
+// embedded in EXIF tag 0x5741 of a non-animated webp sticker.
+func extractWebPStickerMetadata(data []byte) (*StickerMetadata, error) {
+ exif, err := findWebPChunk(data, "EXIF")
+ if err != nil {
+ return nil, err
+ }
+ raw, err := findEXIFTagValue(exif, stickerMetadataEXIFTag)
+ if err != nil {
+ return nil, err
+ }
+ var meta StickerMetadata
+ err = json.Unmarshal(raw, &meta)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse sticker metadata JSON: %w", err)
+ }
+ return &meta, nil
+}
+
+func findWebPChunk(data []byte, chunkType string) ([]byte, error) {
+ if len(data) < 12 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WEBP" {
+ return nil, fmt.Errorf("not a webp file")
+ }
+ for pos := 12; pos+8 <= len(data); {
+ size := binary.LittleEndian.Uint32(data[pos+4 : pos+8])
+ start := pos + 8
+ end := start + int(size)
+ if end > len(data) {
+ return nil, fmt.Errorf("webp chunk %q extends past end of file", data[pos:pos+4])
+ }
+ if string(data[pos:pos+4]) == chunkType {
+ return data[start:end], nil
+ }
+ pos = end
+ if pos%2 != 0 {
+ pos++
+ }
+ }
+ return nil, fmt.Errorf("webp chunk %q not found", chunkType)
+}
+
+func findEXIFTagValue(exif []byte, tag uint16) ([]byte, error) {
+ if len(exif) < 8 {
+ return nil, fmt.Errorf("exif data too short")
+ }
+ var bo binary.ByteOrder
+ switch string(exif[0:2]) {
+ case "II":
+ bo = binary.LittleEndian
+ case "MM":
+ bo = binary.BigEndian
+ default:
+ return nil, fmt.Errorf("invalid TIFF byte order %q", exif[0:2])
+ }
+ if bo.Uint16(exif[2:4]) != 0x002A {
+ return nil, fmt.Errorf("invalid TIFF magic")
+ }
+ ifdOffset := int(bo.Uint32(exif[4:8]))
+ if ifdOffset < 0 || ifdOffset+2 > len(exif) {
+ return nil, fmt.Errorf("IFD offset out of range")
+ }
+ count := int(bo.Uint16(exif[ifdOffset : ifdOffset+2]))
+ entries := ifdOffset + 2
+ if entries+count*12 > len(exif) {
+ return nil, fmt.Errorf("IFD entries out of range")
+ }
+ for i := 0; i < count; i++ {
+ entry := exif[entries+i*12 : entries+(i+1)*12]
+ if bo.Uint16(entry[0:2]) != tag {
+ continue
+ }
+ // Tag 0x5741 stores JSON as type 7 (UNDEFINED), where size == count bytes.
+ size := int(bo.Uint32(entry[4:8]))
+ if size <= 4 {
+ return entry[8 : 8+size], nil
+ }
+ offset := int(bo.Uint32(entry[8:12]))
+ if offset+size > len(exif) {
+ return nil, fmt.Errorf("exif tag value out of range")
+ }
+ return exif[offset : offset+size], nil
+ }
+ return nil, fmt.Errorf("exif tag 0x%04x not found", tag)
+}
From 3f568f0133a50e7b64b651a91d8dae59e7558ff7 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 29 Apr 2026 09:10:15 +0300
Subject: [PATCH 15/94] 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 3054e5a..f499a24 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.27.1-0.20260428110059-49a05bf06436
+ maunium.net/go/mautrix v0.27.1-0.20260429060852-d7aad0e862c7
)
require (
diff --git a/go.sum b/go.sum
index ebe18fa..38542dc 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.27.1-0.20260428110059-49a05bf06436 h1:vga9ypiOLJmGguxq4D1aquDPFihOuD99EGPEwva12UI=
-maunium.net/go/mautrix v0.27.1-0.20260428110059-49a05bf06436/go.mod h1:4fZ0M0xB5ZtueQI65RilX28J/3794BeK+LaCg4U61Jk=
+maunium.net/go/mautrix v0.27.1-0.20260429060852-d7aad0e862c7 h1:ZL/dTgBuj7ZzH543brFUvxZo2lJGsCMBvnfKIvjdHC4=
+maunium.net/go/mautrix v0.27.1-0.20260429060852-d7aad0e862c7/go.mod h1:4fZ0M0xB5ZtueQI65RilX28J/3794BeK+LaCg4U61Jk=
From 0632478ce0deb14fa0e6860655cfc123a5626100 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 29 Apr 2026 19:04:18 +0300
Subject: [PATCH 16/94] 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 f499a24..63b26bc 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.27.1-0.20260429060852-d7aad0e862c7
+ maunium.net/go/mautrix v0.27.1-0.20260429160319-674a25f9b6ee
)
require (
diff --git a/go.sum b/go.sum
index 38542dc..44ae65d 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.27.1-0.20260429060852-d7aad0e862c7 h1:ZL/dTgBuj7ZzH543brFUvxZo2lJGsCMBvnfKIvjdHC4=
-maunium.net/go/mautrix v0.27.1-0.20260429060852-d7aad0e862c7/go.mod h1:4fZ0M0xB5ZtueQI65RilX28J/3794BeK+LaCg4U61Jk=
+maunium.net/go/mautrix v0.27.1-0.20260429160319-674a25f9b6ee h1:OiKSGPfWLQYir1QmvkMvfo/0Dh78hVo8boGwU0Ub32k=
+maunium.net/go/mautrix v0.27.1-0.20260429160319-674a25f9b6ee/go.mod h1:4fZ0M0xB5ZtueQI65RilX28J/3794BeK+LaCg4U61Jk=
From 60a46fd81bf72cea06f9dd71152ef7cd019912cb Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 30 Apr 2026 13:23:21 +0300
Subject: [PATCH 17/94] .github: add another item to bug report template
[skip ci]
---
.github/ISSUE_TEMPLATE/bug.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
index c10630f..06ba9e8 100644
--- a/.github/ISSUE_TEMPLATE/bug.md
+++ b/.github/ISSUE_TEMPLATE/bug.md
@@ -11,7 +11,8 @@ type: Bug
### Checklist
-
+
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
+* [ ] The bug is still present on the main branch.
From 42e83b1ea9f16a916a27ecab60c2ae4b082ab359 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Mon, 4 May 2026 17:01:16 +0300
Subject: [PATCH 18/94] dependencies: update
---
go.mod | 8 ++++----
go.sum | 16 ++++++++--------
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/go.mod b/go.mod
index 63b26bc..2a66a9d 100644
--- a/go.mod
+++ b/go.mod
@@ -8,17 +8,17 @@ tool go.mau.fi/util/cmd/maubuild
require (
github.com/lib/pq v1.12.3
- github.com/rs/zerolog v1.35.0
+ github.com/rs/zerolog v1.35.1
github.com/tidwall/gjson v1.18.0
- go.mau.fi/util v0.9.9-0.20260428124215-c47a7212562e
+ go.mau.fi/util v0.9.9-0.20260501211038-7535d5590b78
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260427122815-7514259253a7
+ go.mau.fi/whatsmeow v0.0.0-20260504140538-51dcc5e33be0
golang.org/x/image v0.39.0
golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.27.1-0.20260429160319-674a25f9b6ee
+ maunium.net/go/mautrix v0.27.1-0.20260502202615-25947505f4a2
)
require (
diff --git a/go.sum b/go.sum
index 44ae65d..e22300a 100644
--- a/go.sum
+++ b/go.sum
@@ -46,8 +46,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
-github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
-github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
+github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
+github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/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=
@@ -71,12 +71,12 @@ github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
-go.mau.fi/util v0.9.9-0.20260428124215-c47a7212562e h1:o0O9sLa4CeZbxbgoSqavwaORrt9BB+trOLKBSoGzJ3Q=
-go.mau.fi/util v0.9.9-0.20260428124215-c47a7212562e/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0=
+go.mau.fi/util v0.9.9-0.20260501211038-7535d5590b78 h1:MRz5RQxXgohVSulsFHqokfZDJzhqk1w+fDQxJksxbZc=
+go.mau.fi/util v0.9.9-0.20260501211038-7535d5590b78/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260427122815-7514259253a7 h1:jEOI4I7kU+MYUNI1L94rhYXhUg8N9+YUNHVY525aYTc=
-go.mau.fi/whatsmeow v0.0.0-20260427122815-7514259253a7/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
+go.mau.fi/whatsmeow v0.0.0-20260504140538-51dcc5e33be0 h1:LIqLhtTxsOsJbQn+WQVqQjgVQJgWYRGQptLJG1DN0/Y=
+go.mau.fi/whatsmeow v0.0.0-20260504140538-51dcc5e33be0/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
@@ -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.27.1-0.20260429160319-674a25f9b6ee h1:OiKSGPfWLQYir1QmvkMvfo/0Dh78hVo8boGwU0Ub32k=
-maunium.net/go/mautrix v0.27.1-0.20260429160319-674a25f9b6ee/go.mod h1:4fZ0M0xB5ZtueQI65RilX28J/3794BeK+LaCg4U61Jk=
+maunium.net/go/mautrix v0.27.1-0.20260502202615-25947505f4a2 h1:yuYNi5X7baSlW/rDXlTV1n+x72uMVwPCubNnMG5wrqk=
+maunium.net/go/mautrix v0.27.1-0.20260502202615-25947505f4a2/go.mod h1:t9xgVOeRTI3QAX04dBEM6iql+SnOOLdIy2jaKWyL2M0=
From e13f63e6b827ad2a0fefc7d3604e771f72891221 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 6 May 2026 13:14:07 +0300
Subject: [PATCH 19/94] dependencies: update
---
go.mod | 8 ++++----
go.sum | 16 ++++++++--------
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/go.mod b/go.mod
index 2a66a9d..d55b65c 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.18.0
- go.mau.fi/util v0.9.9-0.20260501211038-7535d5590b78
+ go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260504140538-51dcc5e33be0
+ go.mau.fi/whatsmeow v0.0.0-20260506100936-a763037b215a
golang.org/x/image v0.39.0
golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.27.1-0.20260502202615-25947505f4a2
+ maunium.net/go/mautrix v0.27.1-0.20260506130904-37580129eaaf
)
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.42 // indirect
+ github.com/mattn/go-sqlite3 v1.14.44 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect
diff --git a/go.sum b/go.sum
index e22300a..bbef170 100644
--- a/go.sum
+++ b/go.sum
@@ -34,8 +34,8 @@ 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.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
-github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
+github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
+github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -71,12 +71,12 @@ github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
-go.mau.fi/util v0.9.9-0.20260501211038-7535d5590b78 h1:MRz5RQxXgohVSulsFHqokfZDJzhqk1w+fDQxJksxbZc=
-go.mau.fi/util v0.9.9-0.20260501211038-7535d5590b78/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0=
+go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0 h1:stkCMpY3ULN6sNrPoRYZ5AQ/kc20a7pmhv6t0sdyVhE=
+go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260504140538-51dcc5e33be0 h1:LIqLhtTxsOsJbQn+WQVqQjgVQJgWYRGQptLJG1DN0/Y=
-go.mau.fi/whatsmeow v0.0.0-20260504140538-51dcc5e33be0/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
+go.mau.fi/whatsmeow v0.0.0-20260506100936-a763037b215a h1:DfD7BXe4m+MIPAe0TjFb8hFUd42CqybeWaTvOH6dMiw=
+go.mau.fi/whatsmeow v0.0.0-20260506100936-a763037b215a/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
@@ -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.27.1-0.20260502202615-25947505f4a2 h1:yuYNi5X7baSlW/rDXlTV1n+x72uMVwPCubNnMG5wrqk=
-maunium.net/go/mautrix v0.27.1-0.20260502202615-25947505f4a2/go.mod h1:t9xgVOeRTI3QAX04dBEM6iql+SnOOLdIy2jaKWyL2M0=
+maunium.net/go/mautrix v0.27.1-0.20260506130904-37580129eaaf h1:AXGEYhQsiQArdtD1XE0NnGIl614j9stHXFmaB+Kb8Sw=
+maunium.net/go/mautrix v0.27.1-0.20260506130904-37580129eaaf/go.mod h1:2ANjihDB+wv2UAqJapkRekmNXw7khSisccAkE5Jg3P0=
From 0f6e7a522c3ae2ccdcd3928a39e6ede8ef513524 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 6 May 2026 13:23:02 +0300
Subject: [PATCH 20/94] .github: add version command to bug report template
---
.github/ISSUE_TEMPLATE/bug.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
index 06ba9e8..4b3b934 100644
--- a/.github/ISSUE_TEMPLATE/bug.md
+++ b/.github/ISSUE_TEMPLATE/bug.md
@@ -15,4 +15,4 @@ type: Bug
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
-* [ ] The bug is still present on the main branch.
+* [ ] The bug is still present on the main branch. The `!wa version` command output is: ``
From ac2912a14536d80453ec57390465edb05afcb71f Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 6 May 2026 16:03:28 +0300
Subject: [PATCH 21/94] msgconv,directmedia: add support for importing image
packs
---
go.mod | 2 +-
go.sum | 4 +-
pkg/connector/capabilities.go | 4 +-
pkg/connector/client.go | 11 +++
pkg/connector/directmedia.go | 47 ++++++++--
pkg/connector/handlematrix.go | 1 +
pkg/msgconv/from-matrix.go | 38 +++++++-
pkg/msgconv/msgconv.go | 11 ++-
pkg/msgconv/wa-media.go | 14 ++-
pkg/msgconv/wa-sticker.go | 172 +++++++++++++++++++++++++++++++++-
pkg/waid/mediaid.go | 42 +++++++++
11 files changed, 323 insertions(+), 23 deletions(-)
diff --git a/go.mod b/go.mod
index d55b65c..1f12612 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
github.com/tidwall/gjson v1.18.0
go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260506100936-a763037b215a
+ go.mau.fi/whatsmeow v0.0.0-20260506122147-6a7198d94d26
golang.org/x/image v0.39.0
golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
diff --git a/go.sum b/go.sum
index bbef170..08791dd 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,8 @@ go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0 h1:stkCMpY3ULN6sNrPoRYZ5AQ/k
go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260506100936-a763037b215a h1:DfD7BXe4m+MIPAe0TjFb8hFUd42CqybeWaTvOH6dMiw=
-go.mau.fi/whatsmeow v0.0.0-20260506100936-a763037b215a/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
+go.mau.fi/whatsmeow v0.0.0-20260506122147-6a7198d94d26 h1:DyFksXWn7z/NN+TNJ0DomV1/drWjkyiVuJ6RIiy/bo4=
+go.mau.fi/whatsmeow v0.0.0-20260506122147-6a7198d94d26/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go
index 202569b..d8b1367 100644
--- a/pkg/connector/capabilities.go
+++ b/pkg/connector/capabilities.go
@@ -125,10 +125,10 @@ var whatsappCaps = &event.RoomFeatures{
event.CapMsgSticker: {
MimeTypes: map[string]event.CapabilitySupportLevel{
"image/webp": event.CapLevelFullySupported,
- // TODO see if sending lottie is possible
- //"video/lottie+json": event.CapLevelFullySupported,
"image/png": event.CapLevelPartialSupport,
"image/jpeg": event.CapLevelPartialSupport,
+ // This will only be accepted if it was imported from WhatsApp
+ "video/lottie+json": event.CapLevelPartialSupport,
},
Caption: event.CapLevelDropped,
MaxSize: WAMaxFileSize,
diff --git a/pkg/connector/client.go b/pkg/connector/client.go
index 367ad14..3f19bf7 100644
--- a/pkg/connector/client.go
+++ b/pkg/connector/client.go
@@ -38,6 +38,7 @@ import (
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status"
+ "maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)
@@ -128,6 +129,7 @@ var (
_ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil)
+ _ bridgev2.StickerImportingNetworkAPI = (*WhatsAppClient)(nil)
)
var pushCfg = &bridgev2.PushConfig{
@@ -467,3 +469,12 @@ func (wa *WhatsAppClient) updatePresence(ctx context.Context, presence types.Pre
}
return err
}
+
+func (wa *WhatsAppClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
+ return wa.Main.MsgConv.DownloadImagePack(ctx, wa.UserLogin.ID, wa.Client, url)
+}
+
+func (wa *WhatsAppClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) {
+ // TODO
+ return nil, nil
+}
diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go
index 10cdbbb..ac2fbd2 100644
--- a/pkg/connector/directmedia.go
+++ b/pkg/connector/directmedia.go
@@ -67,6 +67,8 @@ func (wa *WhatsAppConnector) Download(ctx context.Context, mediaID networkid.Med
return wa.downloadMessageDirectMedia(ctx, parsedID, params)
} else if parsedID.Avatar != nil {
return wa.downloadAvatarDirectMedia(ctx, parsedID, params)
+ } else if parsedID.Sticker != nil {
+ return wa.downloadStickerDirectMedia(ctx, parsedID, params)
} else {
return nil, fmt.Errorf("unexpected media ID parsing result")
}
@@ -135,8 +137,25 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars
}, nil
}
+func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
+ ul := wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin)
+ if ul == nil {
+ return nil, fmt.Errorf("%w: user login %s not found", bridgev2.ErrNotLoggedIn, parsedID.UserLogin)
+ }
+ waClient := ul.Client.(*WhatsAppClient)
+ if waClient.Client == nil {
+ return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin)
+ }
+ sticker, err := wa.MsgConv.GetCachedSticker(ctx, waClient.Client, parsedID.Sticker.PackID, parsedID.Sticker.FileHash)
+ if err != nil {
+ return nil, err
+ } else if sticker == nil {
+ return nil, mautrix.MNotFound.WithMessage("Sticker not found in pack")
+ }
+ return wa.makeDirectMediaResponse(ctx, waClient, sticker, sticker.MimeType, "", nil, params)
+}
+
func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
- log := zerolog.Ctx(ctx)
msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String())
if err != nil {
return nil, fmt.Errorf("failed to get message: %w", err)
@@ -174,16 +193,29 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
if waClient.Client == nil {
return nil, fmt.Errorf("no WhatsApp client found on login")
}
+ return wa.makeDirectMediaResponse(ctx, waClient, keys, keys.MimeType, msg.ID, keys, params)
+}
+
+func (wa *WhatsAppConnector) makeDirectMediaResponse(
+ ctx context.Context,
+ waClient *WhatsAppClient,
+ dm whatsmeow.DownloadableMessage,
+ mimeType string,
+ msgID networkid.MessageID,
+ keys *msgconv.FailedMediaKeys,
+ params map[string]string,
+) (mediaproxy.GetMediaResponse, error) {
return &mediaproxy.GetMediaResponseFile{
Callback: func(f *os.File) (*mediaproxy.FileMeta, error) {
- err := waClient.Client.DownloadToFile(ctx, keys, f)
- if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent) {
+ 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"]
if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") {
return nil, ErrReloadNeeded
}
log.Trace().Msg("Media not found for direct download, requesting and waiting")
- err = waClient.requestAndWaitDirectMedia(ctx, msg.ID, keys)
+ err = waClient.requestAndWaitDirectMedia(ctx, msgID, keys)
if err != nil {
log.Trace().Err(err).Msg("Failed to wait for media for direct download")
return nil, err
@@ -197,8 +229,7 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
return nil, err
}
- mime := keys.MimeType
- if mime == "application/was" {
+ if mimeType == "application/was" {
if _, err := f.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err)
} else if zipData, err := io.ReadAll(f); err != nil {
@@ -210,11 +241,11 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
} else if err := f.Truncate(int64(len(data))); err != nil {
return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err)
}
- mime = "video/lottie+json"
+ mimeType = "video/lottie+json"
}
return &mediaproxy.FileMeta{
- ContentType: mime,
+ ContentType: mimeType,
}, nil
},
}, nil
diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go
index a0a67fa..b0963ec 100644
--- a/pkg/connector/handlematrix.go
+++ b/pkg/connector/handlematrix.go
@@ -107,6 +107,7 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID)
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2))
+ zerolog.Ctx(ctx).Trace().Any("payload", waMsg).Msg("Outgoing message payload")
resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req)
if err != nil {
return nil, err
diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go
index 49f6048..4eb0a88 100644
--- a/pkg/msgconv/from-matrix.go
+++ b/pkg/msgconv/from-matrix.go
@@ -19,6 +19,7 @@ package msgconv
import (
"bytes"
"context"
+ "encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -201,6 +202,7 @@ func (mc *MessageConverter) constructMediaMessage(
FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uploaded.FileLength),
URL: proto.String(uploaded.URL),
+ IsLottie: proto.Bool(mime == "application/was"),
},
}
case event.MsgAudio:
@@ -482,6 +484,17 @@ func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) {
return webpBuffer.Bytes(), size, nil
}
+func (mc *MessageConverter) getOriginalBridgedSticker(ctx context.Context, info *event.BridgedSticker) (*types.StickerPackItem, error) {
+ if info == nil || info.Network != StickerSourceID || !strings.HasPrefix(info.PackURL, StickerPackURLPrefix) || info.ID == "" {
+ return nil, nil
+ }
+ fileHash, err := base64.StdEncoding.DecodeString(info.ID)
+ if err != nil {
+ return nil, nil
+ }
+ return mc.GetCachedSticker(ctx, getClient(ctx), strings.TrimPrefix(info.PackURL, StickerPackURLPrefix), fileHash)
+}
+
func (mc *MessageConverter) reuploadFileToWhatsApp(
ctx context.Context, content *event.MessageEventContent,
) (*whatsmeow.UploadResponse, []byte, string, error) {
@@ -490,7 +503,21 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
if content.FileName != "" {
fileName = content.FileName
}
- data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
+ var data []byte
+ var err error
+ var sticker *types.StickerPackItem
+ if sticker, err = mc.getOriginalBridgedSticker(ctx, content.Info.BridgedSticker); sticker != nil && sticker.MimeType == "application/was" {
+ data, err = getClient(ctx).Download(ctx, sticker)
+ mime = sticker.MimeType
+ content.Info.Width = sticker.Width
+ content.Info.Height = sticker.Height
+ } else {
+ if err != nil {
+ zerolog.Ctx(ctx).Warn().Err(err).
+ Msg("Failed to get original bridged sticker, falling back to downloading from URL")
+ }
+ data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
+ }
if err != nil {
return nil, nil, "", fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
}
@@ -508,7 +535,14 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
case event.MessageType(event.EventSticker.Type):
isSticker = true
mediaType = whatsmeow.MediaImage
- if mime != "image/webp" || content.Info.Width != content.Info.Height {
+ if mime == "video/lottie+json" {
+ // This likely won't work
+ data, err = PackAnimatedSticker(data)
+ if err != nil {
+ return nil, nil, mime, fmt.Errorf("%w (packing animated sticker): %w", bridgev2.ErrMediaConvertFailed, err)
+ }
+ mime = "application/was"
+ } else if (mime != "image/webp" || content.Info.Width != content.Info.Height) && mime != "application/was" {
var size int
data, size, err = mc.convertToWebP(data)
if err != nil {
diff --git a/pkg/msgconv/msgconv.go b/pkg/msgconv/msgconv.go
index e4109c8..185ee0f 100644
--- a/pkg/msgconv/msgconv.go
+++ b/pkg/msgconv/msgconv.go
@@ -17,6 +17,9 @@
package msgconv
import (
+ "sync"
+
+ "go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/format"
@@ -43,12 +46,16 @@ type MessageConverter struct {
DisableViewOnce bool
DirectMedia bool
OldMediaSuffix string
+
+ stickerPackCache map[string]*types.StickerPack
+ stickerPackCacheLock sync.Mutex
}
func New(br *bridgev2.Bridge) *MessageConverter {
mc := &MessageConverter{
- Bridge: br,
- MaxFileSize: 50 * 1024 * 1024,
+ Bridge: br,
+ MaxFileSize: 50 * 1024 * 1024,
+ stickerPackCache: make(map[string]*types.StickerPack),
}
mc.HTMLParser = &format.HTMLParser{
PillConverter: mc.convertPill,
diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go
index d1b5332..03bb274 100644
--- a/pkg/msgconv/wa-media.go
+++ b/pkg/msgconv/wa-media.go
@@ -35,6 +35,7 @@ import (
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/event"
+ "maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)
@@ -81,11 +82,11 @@ func (mc *MessageConverter) convertMediaMessage(
MimeType: msg.GetMimetype(),
}
if mc.DirectMedia {
- preparedMedia.FillFileName()
if preparedMedia.Info.MimeType == "application/was" {
preparedMedia.Info.MimeType = "video/lottie+json"
preparedMedia.FileName = "sticker.json"
}
+ preparedMedia.FillFileName()
var err error
portal := getPortal(ctx)
idOverride := getEditTargetID(ctx)
@@ -352,12 +353,15 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
) error {
client := getClient(ctx)
intent := getIntent(ctx)
- portal := getPortal(ctx)
+ var roomID id.RoomID
+ if portal := getPortal(ctx); portal != nil {
+ roomID = portal.MXID
+ }
var thumbnailData []byte
var thumbnailInfo *event.FileInfo
if part.Info.Size > uploadFileThreshold {
var err error
- part.URL, part.File, err = intent.UploadMediaStream(ctx, portal.MXID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
+ part.URL, part.File, err = intent.UploadMediaStream(ctx, roomID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
err := client.DownloadToFile(ctx, message, file.(*os.File))
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
@@ -397,7 +401,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
part.Info.MimeType = http.DetectContentType(data)
}
part.FillFileName()
- part.URL, part.File, err = intent.UploadMedia(ctx, portal.MXID, data, part.FileName, part.Info.MimeType)
+ part.URL, part.File, err = intent.UploadMedia(ctx, roomID, data, part.FileName, part.Info.MimeType)
if err != nil {
return fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err)
}
@@ -406,7 +410,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
var err error
part.Info.ThumbnailURL, part.Info.ThumbnailFile, err = intent.UploadMedia(
ctx,
- portal.MXID,
+ roomID,
thumbnailData,
"thumbnail"+exmime.ExtensionFromMimetype(thumbnailInfo.MimeType),
thumbnailInfo.MimeType,
diff --git a/pkg/msgconv/wa-sticker.go b/pkg/msgconv/wa-sticker.go
index fef111a..85766ed 100644
--- a/pkg/msgconv/wa-sticker.go
+++ b/pkg/msgconv/wa-sticker.go
@@ -20,10 +20,13 @@ import (
"archive/zip"
"bytes"
"context"
+ "encoding/base64"
"encoding/binary"
"encoding/json"
+ "errors"
"fmt"
"io"
+ "net/url"
"os"
"path/filepath"
"strconv"
@@ -34,9 +37,158 @@ import (
"go.mau.fi/util/exstrings"
"go.mau.fi/util/lottie"
"go.mau.fi/util/random"
+ "go.mau.fi/whatsmeow"
+ "go.mau.fi/whatsmeow/types"
+ "maunium.net/go/mautrix"
+ "maunium.net/go/mautrix/bridgev2"
+ "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
+
+ "go.mau.fi/mautrix-whatsapp/pkg/waid"
)
+func (mc *MessageConverter) GetCachedStickerPack(ctx context.Context, client *whatsmeow.Client, packID string) (*types.StickerPack, error) {
+ mc.stickerPackCacheLock.Lock()
+ defer mc.stickerPackCacheLock.Unlock()
+ cached, ok := mc.stickerPackCache[packID]
+ if ok {
+ if cached == nil {
+ return nil, bridgev2.RespError(mautrix.MNotFound.WithMessage("sticker pack not found (cached)"))
+ }
+ return cached, nil
+ }
+
+ pack, err := client.FetchStickerPack(ctx, packID)
+ if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) {
+ mc.stickerPackCache[packID] = nil
+ return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
+ } else if err != nil {
+ return nil, err
+ }
+ mc.stickerPackCache[packID] = pack
+ if packID != pack.StickerPackID {
+ mc.stickerPackCache[pack.StickerPackID] = pack
+ }
+ return pack, nil
+}
+
+func (mc *MessageConverter) GetCachedSticker(ctx context.Context, client *whatsmeow.Client, packID string, hash []byte) (*types.StickerPackItem, error) {
+ pack, err := mc.GetCachedStickerPack(ctx, client, packID)
+ if err != nil {
+ return nil, err
+ }
+ for _, sticker := range pack.Stickers {
+ if bytes.Equal(sticker.FileHash, hash) {
+ return sticker, nil
+ }
+ }
+ return nil, nil
+}
+
+func (mc *MessageConverter) DownloadImagePack(ctx context.Context, userLoginID networkid.UserLoginID, client *whatsmeow.Client, inputURL string) (*bridgev2.ImportedImagePack, error) {
+ parsedURL, err := url.Parse(inputURL)
+ if err != nil {
+ return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
+ } else if parsedURL.Host != "api.whatsapp.com" && parsedURL.Host != "wa.me" {
+ return nil, bridgev2.WrapRespErr(fmt.Errorf("invalid host %q", parsedURL.Host), mautrix.MNotFound)
+ } else if !strings.HasPrefix(parsedURL.Path, "/stickerpack/") {
+ return nil, bridgev2.WrapRespErr(fmt.Errorf("invalid path %q", parsedURL.Path), mautrix.MNotFound)
+ }
+ packName := strings.Split(strings.TrimPrefix(parsedURL.Path, "/stickerpack/"), "/")[0]
+ if packName == "" {
+ return nil, bridgev2.WrapRespErr(fmt.Errorf("empty pack name"), mautrix.MNotFound)
+ }
+ pack, err := mc.GetCachedStickerPack(ctx, client, packName)
+ if err != nil {
+ return nil, err
+ }
+ canonicalURL := "https://wa.me/stickerpack/" + pack.StickerPackID
+ topLevelExtra := map[string]any{
+ "fi.mau.whatsapp.stickerpack": map[string]any{
+ "id": pack.StickerPackID,
+ "name": pack.Name,
+ "description": pack.Description,
+ "publisher": pack.Publisher,
+ "animated": pack.Animated > 0,
+ "lottie": pack.Lottie > 0,
+ },
+ }
+ content := &event.ImagePackEventContent{
+ Images: make(map[string]*event.ImagePackImage, len(pack.Stickers)),
+ Metadata: event.ImagePackMetadata{
+ DisplayName: pack.Name,
+ AvatarURL: "",
+ Usage: []event.ImagePackUsage{event.ImagePackUsageSticker},
+ Attribution: fmt.Sprintf("By %s on WhatsApp %s", pack.Publisher, canonicalURL),
+ BridgedPack: &event.BridgedStickerPack{
+ Network: StickerSourceID,
+ URL: canonicalURL,
+ },
+ },
+ }
+ ctx = context.WithValue(ctx, contextKeyClient, client)
+ ctx = context.WithValue(ctx, contextKeyIntent, mc.Bridge.Bot)
+ ctx = context.WithValue(ctx, contextKeyPortal, (*bridgev2.Portal)(nil))
+ for i, sticker := range pack.Stickers {
+ shortcode := sticker.PreviewWebpID
+ if shortcode == "" {
+ shortcode = fmt.Sprintf("%s_img%d", pack.StickerPackID, i+1)
+ }
+ body := sticker.AccessibilityText
+ var emoji string
+ if len(sticker.Emojis) > 0 {
+ emoji = sticker.Emojis[0]
+ if body == "" {
+ body = strings.Join(sticker.Emojis, " ")
+ }
+ }
+ part := &PreparedMedia{
+ Type: event.EventSticker,
+ MessageEventContent: &event.MessageEventContent{
+ Body: body,
+ Info: &event.FileInfo{
+ MimeType: sticker.MimeType,
+ Width: sticker.Width,
+ Height: sticker.Height,
+ Size: int(sticker.FileSize),
+ BridgedSticker: &event.BridgedSticker{
+ Network: StickerSourceID,
+ ID: base64.StdEncoding.EncodeToString(sticker.FileHash),
+ Emoji: emoji,
+ PackURL: canonicalURL,
+ },
+ },
+ },
+ TypeDescription: "sticker",
+ }
+ if mc.DirectMedia {
+ if part.Info.MimeType == "application/was" {
+ part.Info.MimeType = "video/lottie+json"
+ }
+ part.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeStickerPackMediaID(pack.StickerPackID, sticker.FileHash, userLoginID))
+ if err != nil {
+ panic(fmt.Errorf("failed to generate content URI: %w", err))
+ }
+ } else {
+ err = mc.reuploadWhatsAppAttachment(ctx, sticker, part)
+ if err != nil {
+ return nil, fmt.Errorf("failed to reupload sticker %q: %w", sticker.GetDirectPath(), err)
+ }
+ }
+ content.Images[shortcode] = &event.ImagePackImage{
+ URL: part.URL,
+ Body: part.Body,
+ Info: part.Info,
+ }
+ }
+
+ return &bridgev2.ImportedImagePack{
+ Content: content,
+ Extra: topLevelExtra,
+ Shortcode: pack.StickerPackID,
+ }, nil
+}
+
type StickerMetadata struct {
StickerPackID string `json:"sticker-pack-id"`
AccessibilityText string `json:"accessibility-text"`
@@ -48,7 +200,7 @@ func (sm *StickerMetadata) ToMatrix(content *event.MessageEventContent) {
if sm == nil {
return
}
- if sm.StickerPackID != "" {
+ if sm.StickerPackID != "" && content.Info.BridgedSticker == nil {
content.Info.BridgedSticker = &event.BridgedSticker{
Network: StickerSourceID,
PackURL: StickerPackURLPrefix + sm.StickerPackID,
@@ -67,6 +219,24 @@ func (sm *StickerMetadata) ToMatrix(content *event.MessageEventContent) {
const StickerSourceID = "whatsapp"
const StickerPackURLPrefix = "https://wa.me/stickerpack/"
+func PackAnimatedSticker(data []byte) ([]byte, error) {
+ var buf bytes.Buffer
+ zipWriter := zip.NewWriter(&buf)
+ f, err := zipWriter.Create("animation/animation.json")
+ if err != nil {
+ return nil, fmt.Errorf("failed to create zip entry: %w", err)
+ }
+ _, err = f.Write(data)
+ if err != nil {
+ return nil, fmt.Errorf("failed to write zip entry: %w", err)
+ }
+ err = zipWriter.Close()
+ if err != nil {
+ return nil, fmt.Errorf("failed to close zip writer: %w", err)
+ }
+ return buf.Bytes(), nil
+}
+
func ExtractAnimatedSticker(data []byte) ([]byte, *StickerMetadata, error) {
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
diff --git a/pkg/waid/mediaid.go b/pkg/waid/mediaid.go
index 6a94de4..093ece1 100644
--- a/pkg/waid/mediaid.go
+++ b/pkg/waid/mediaid.go
@@ -33,6 +33,7 @@ const (
mediaIDTypeMessage = 255
mediaIDTypeAvatar = 254
mediaIDTypeCommunityAvatar = 253
+ mediaIDTypeStickerPackItem = 252
)
func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID) networkid.MediaID {
@@ -82,9 +83,28 @@ type AvatarMediaInfo struct {
Community bool
}
+func MakeStickerPackMediaID(packID string, fileHash []byte, receiver networkid.UserLoginID) networkid.MediaID {
+ receiverID := compactJID(ParseUserLoginID(receiver, 0))
+ mediaID := make([]byte, 0, 4+len(packID)+len(fileHash)+len(receiverID))
+ mediaID = append(mediaID, mediaIDTypeStickerPackItem)
+ mediaID = append(mediaID, byte(len(packID)))
+ mediaID = append(mediaID, packID...)
+ mediaID = append(mediaID, byte(len(fileHash)))
+ mediaID = append(mediaID, fileHash...)
+ mediaID = append(mediaID, byte(len(receiverID)))
+ mediaID = append(mediaID, receiverID...)
+ return mediaID
+}
+
+type StickerPackMediaInfo struct {
+ PackID string
+ FileHash []byte
+}
+
type ParsedMediaID struct {
Message *ParsedMessageID
Avatar *AvatarMediaInfo
+ Sticker *StickerPackMediaInfo
UserLogin networkid.UserLoginID
}
@@ -138,6 +158,24 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) {
Community: mediaIDType == mediaIDTypeCommunityAvatar,
}
parsed.UserLogin = MakeUserLoginID(receiverID)
+ case mediaIDTypeStickerPackItem:
+ packID, err := readCompact(&mediaID, parseString)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse sticker pack ID: %w", err)
+ }
+ fileHash, err := readCompact(&mediaID, rawBytes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse sticker file hash: %w", err)
+ }
+ receiverID, err := readCompact(&mediaID, parseCompactJID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse receiver JID: %w", err)
+ }
+ parsed.Sticker = &StickerPackMediaInfo{
+ PackID: packID,
+ FileHash: fileHash,
+ }
+ parsed.UserLogin = MakeUserLoginID(receiverID)
default:
return nil, fmt.Errorf("unknown media ID type %d", mediaIDType)
}
@@ -246,6 +284,10 @@ func parseCompactJID(jid []byte) (types.JID, error) {
}
}
+func rawBytes(data []byte) ([]byte, error) {
+ return data, nil
+}
+
func readCompact[T any](data *networkid.MediaID, fn func(data []byte) (T, error)) (T, error) {
var defVal T
if len(*data) < 1 {
From aeeea94e8ea0eb7740a470a6f5e41e51d20d27d4 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 6 May 2026 16:12:40 +0300
Subject: [PATCH 22/94] msgconv/wa-sticker: apply correct dimensions to
imported packs
---
pkg/msgconv/from-matrix.go | 18 +++++++++++-------
pkg/msgconv/wa-media.go | 24 ++++++++++++++----------
pkg/msgconv/wa-sticker.go | 1 +
3 files changed, 26 insertions(+), 17 deletions(-)
diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go
index 4eb0a88..93de90c 100644
--- a/pkg/msgconv/from-matrix.go
+++ b/pkg/msgconv/from-matrix.go
@@ -506,16 +506,20 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
var data []byte
var err error
var sticker *types.StickerPackItem
- if sticker, err = mc.getOriginalBridgedSticker(ctx, content.Info.BridgedSticker); sticker != nil && sticker.MimeType == "application/was" {
- data, err = getClient(ctx).Download(ctx, sticker)
- mime = sticker.MimeType
+ if sticker, err = mc.getOriginalBridgedSticker(ctx, content.Info.BridgedSticker); err != nil {
+ zerolog.Ctx(ctx).Warn().Err(err).
+ Msg("Failed to get original bridged sticker, falling back to downloading from URL")
+ data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
+ } else if sticker != nil {
+ if sticker.MimeType == "application/was" {
+ data, err = getClient(ctx).Download(ctx, sticker)
+ mime = sticker.MimeType
+ } else {
+ data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
+ }
content.Info.Width = sticker.Width
content.Info.Height = sticker.Height
} else {
- if err != nil {
- zerolog.Ctx(ctx).Warn().Err(err).
- Msg("Failed to get original bridged sticker, falling back to downloading from URL")
- }
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
}
if err != nil {
diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go
index 03bb274..c2a8624 100644
--- a/pkg/msgconv/wa-media.go
+++ b/pkg/msgconv/wa-media.go
@@ -236,6 +236,19 @@ type MediaMessageWithDuration interface {
const WhatsAppStickerSize = 190
+func fixStickerDimensions(info *event.FileInfo) {
+ if info.Width == info.Height {
+ info.Width = WhatsAppStickerSize
+ info.Height = WhatsAppStickerSize
+ } else if info.Width > info.Height {
+ info.Height /= info.Width / WhatsAppStickerSize
+ info.Width = WhatsAppStickerSize
+ } else {
+ info.Width /= info.Height / WhatsAppStickerSize
+ info.Height = WhatsAppStickerSize
+ }
+}
+
func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
extraInfo := map[string]any{}
data := &PreparedMedia{
@@ -284,16 +297,7 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
case *waE2E.StickerMessage:
data.Type = event.EventSticker
data.FileName = "sticker" + exmime.ExtensionFromMimetype(msg.GetMimetype())
- if data.Info.Width == data.Info.Height {
- data.Info.Width = WhatsAppStickerSize
- data.Info.Height = WhatsAppStickerSize
- } else if data.Info.Width > data.Info.Height {
- data.Info.Height /= data.Info.Width / WhatsAppStickerSize
- data.Info.Width = WhatsAppStickerSize
- } else {
- data.Info.Width /= data.Info.Height / WhatsAppStickerSize
- data.Info.Height = WhatsAppStickerSize
- }
+ fixStickerDimensions(data.Info)
case *waE2E.VideoMessage:
data.MsgType = event.MsgVideo
pairedMediaType := msg.GetContextInfo().GetPairedMediaType()
diff --git a/pkg/msgconv/wa-sticker.go b/pkg/msgconv/wa-sticker.go
index 85766ed..e9507bc 100644
--- a/pkg/msgconv/wa-sticker.go
+++ b/pkg/msgconv/wa-sticker.go
@@ -161,6 +161,7 @@ func (mc *MessageConverter) DownloadImagePack(ctx context.Context, userLoginID n
},
TypeDescription: "sticker",
}
+ fixStickerDimensions(part.Info)
if mc.DirectMedia {
if part.Info.MimeType == "application/was" {
part.Info.MimeType = "video/lottie+json"
From fb6ff807a865b9f853378d1b8ab86b3b02b092eb Mon Sep 17 00:00:00 2001
From: SpiritCroc
Date: Thu, 7 May 2026 14:51:09 +0200
Subject: [PATCH 23/94] capabilities: promote image pack import support (#915)
---
pkg/connector/capabilities.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go
index d8b1367..3864356 100644
--- a/pkg/connector/capabilities.go
+++ b/pkg/connector/capabilities.go
@@ -19,6 +19,7 @@ var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true,
ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{
+ ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true,
LookupPhone: true,
From 3f1223cdedd840832a5d5fbdaced4bb4198c4b9a Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 7 May 2026 17:02:00 +0300
Subject: [PATCH 24/94] 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 1f12612..b9f5347 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.27.1-0.20260506130904-37580129eaaf
+ maunium.net/go/mautrix v0.27.1-0.20260507135742-7ec18e08eac3
)
require (
diff --git a/go.sum b/go.sum
index 08791dd..0575b9c 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.27.1-0.20260506130904-37580129eaaf h1:AXGEYhQsiQArdtD1XE0NnGIl614j9stHXFmaB+Kb8Sw=
-maunium.net/go/mautrix v0.27.1-0.20260506130904-37580129eaaf/go.mod h1:2ANjihDB+wv2UAqJapkRekmNXw7khSisccAkE5Jg3P0=
+maunium.net/go/mautrix v0.27.1-0.20260507135742-7ec18e08eac3 h1:K2Aci+LppxMA2CGzj1FQBSzTh2z4F3Kv4l0tKUsaaxs=
+maunium.net/go/mautrix v0.27.1-0.20260507135742-7ec18e08eac3/go.mod h1:2ANjihDB+wv2UAqJapkRekmNXw7khSisccAkE5Jg3P0=
From f5f26e5ef45db052801bf72e1ff2b40ec42337c6 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 8 May 2026 17:16:10 +0300
Subject: [PATCH 25/94] msgconv/wa-sticker: cache converted sticker pack items
---
pkg/msgconv/wa-sticker.go | 26 ++++++++++++++++++++++----
1 file changed, 22 insertions(+), 4 deletions(-)
diff --git a/pkg/msgconv/wa-sticker.go b/pkg/msgconv/wa-sticker.go
index e9507bc..b9797a6 100644
--- a/pkg/msgconv/wa-sticker.go
+++ b/pkg/msgconv/wa-sticker.go
@@ -41,6 +41,7 @@ import (
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
+ "maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
@@ -161,8 +162,11 @@ func (mc *MessageConverter) DownloadImagePack(ctx context.Context, userLoginID n
},
TypeDescription: "sticker",
}
+ dbKey := database.Key(fmt.Sprintf("stickercache:%x", part.Info.BridgedSticker.ID))
fixStickerDimensions(part.Info)
+ var packed *event.ImagePackImage
if mc.DirectMedia {
+ dbKey = ""
if part.Info.MimeType == "application/was" {
part.Info.MimeType = "video/lottie+json"
}
@@ -170,17 +174,31 @@ func (mc *MessageConverter) DownloadImagePack(ctx context.Context, userLoginID n
if err != nil {
panic(fmt.Errorf("failed to generate content URI: %w", err))
}
+ } else if cached := mc.Bridge.DB.KV.Get(ctx, dbKey); cached != "" {
+ err = json.Unmarshal([]byte(cached), &packed)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal cached sticker data: %w", err)
+ }
} else {
err = mc.reuploadWhatsAppAttachment(ctx, sticker, part)
if err != nil {
return nil, fmt.Errorf("failed to reupload sticker %q: %w", sticker.GetDirectPath(), err)
}
}
- content.Images[shortcode] = &event.ImagePackImage{
- URL: part.URL,
- Body: part.Body,
- Info: part.Info,
+ if packed == nil {
+ packed = &event.ImagePackImage{
+ URL: part.URL,
+ Body: part.Body,
+ Info: part.Info,
+ }
+ if dbKey != "" {
+ data, _ := json.Marshal(packed)
+ if data != nil {
+ mc.Bridge.DB.KV.Set(ctx, dbKey, string(data))
+ }
+ }
}
+ content.Images[shortcode] = packed
}
return &bridgev2.ImportedImagePack{
From 1010a4d52060cf37734ed0658c71cac8948ef292 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Mon, 11 May 2026 14:27:06 +0300
Subject: [PATCH 26/94] 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 b9f5347..7ba785b 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
github.com/tidwall/gjson v1.18.0
go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260506122147-6a7198d94d26
+ go.mau.fi/whatsmeow v0.0.0-20260511112314-81f8702130bd
golang.org/x/image v0.39.0
golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
diff --git a/go.sum b/go.sum
index 0575b9c..220edd5 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,8 @@ go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0 h1:stkCMpY3ULN6sNrPoRYZ5AQ/k
go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260506122147-6a7198d94d26 h1:DyFksXWn7z/NN+TNJ0DomV1/drWjkyiVuJ6RIiy/bo4=
-go.mau.fi/whatsmeow v0.0.0-20260506122147-6a7198d94d26/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
+go.mau.fi/whatsmeow v0.0.0-20260511112314-81f8702130bd h1:tqp8Bvki8H9OcoKHDmy94QiQdV7eaiSR/dD9APUlKc0=
+go.mau.fi/whatsmeow v0.0.0-20260511112314-81f8702130bd/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
From 4ae0bd38f2be5366a4b3004b3dede8de0e230a06 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Mon, 11 May 2026 18:57:45 +0300
Subject: [PATCH 27/94] 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 7ba785b..705e578 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
github.com/tidwall/gjson v1.18.0
go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260511112314-81f8702130bd
+ go.mau.fi/whatsmeow v0.0.0-20260511155711-eb05d94dea7d
golang.org/x/image v0.39.0
golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
diff --git a/go.sum b/go.sum
index 220edd5..12e29fb 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,8 @@ go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0 h1:stkCMpY3ULN6sNrPoRYZ5AQ/k
go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260511112314-81f8702130bd h1:tqp8Bvki8H9OcoKHDmy94QiQdV7eaiSR/dD9APUlKc0=
-go.mau.fi/whatsmeow v0.0.0-20260511112314-81f8702130bd/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
+go.mau.fi/whatsmeow v0.0.0-20260511155711-eb05d94dea7d h1:GBtuMd+MvpxZ0hII0xWzc9N4z1BPhph7aDZb6EizhO4=
+go.mau.fi/whatsmeow v0.0.0-20260511155711-eb05d94dea7d/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
From 1a4490843684ea488a06c487b7a04070b47b26e1 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Tue, 12 May 2026 12:24:46 +0300
Subject: [PATCH 28/94] capabilities: bump versions
---
pkg/connector/capabilities.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go
index 3864356..3e6f658 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, 7
+ return 1, 8
}
const WAMaxFileSize = 2000 * 1024 * 1024
@@ -67,7 +67,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
}
func capID() string {
- base := "fi.mau.whatsapp.capabilities.2025_12_15"
+ base := "fi.mau.whatsapp.capabilities.2026_05_12"
if ffmpeg.Supported() {
return base + "+ffmpeg"
}
From 0d02df4cf5b08217945b1898da0c5a85157e4334 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 13 May 2026 15:05:53 +0300
Subject: [PATCH 29/94] dependencies: update mautrix-go
---
go.mod | 4 ++--
go.sum | 8 ++++----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/go.mod b/go.mod
index 705e578..033ecf9 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,7 @@ require (
github.com/lib/pq v1.12.3
github.com/rs/zerolog v1.35.1
github.com/tidwall/gjson v1.18.0
- go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0
+ go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25
go.mau.fi/webp v0.2.0
go.mau.fi/whatsmeow v0.0.0-20260511155711-eb05d94dea7d
golang.org/x/image v0.39.0
@@ -18,7 +18,7 @@ require (
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.27.1-0.20260507135742-7ec18e08eac3
+ maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4
)
require (
diff --git a/go.sum b/go.sum
index 12e29fb..3ea04fb 100644
--- a/go.sum
+++ b/go.sum
@@ -71,8 +71,8 @@ github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
-go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0 h1:stkCMpY3ULN6sNrPoRYZ5AQ/kc20a7pmhv6t0sdyVhE=
-go.mau.fi/util v0.9.9-0.20260505143909-8e67f0d355e0/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
+go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25 h1:YPEmc+li7TF6C9AdRTcSLMb6yCHdF27/wNT7kFLIVNg=
+go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
go.mau.fi/whatsmeow v0.0.0-20260511155711-eb05d94dea7d h1:GBtuMd+MvpxZ0hII0xWzc9N4z1BPhph7aDZb6EizhO4=
@@ -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.27.1-0.20260507135742-7ec18e08eac3 h1:K2Aci+LppxMA2CGzj1FQBSzTh2z4F3Kv4l0tKUsaaxs=
-maunium.net/go/mautrix v0.27.1-0.20260507135742-7ec18e08eac3/go.mod h1:2ANjihDB+wv2UAqJapkRekmNXw7khSisccAkE5Jg3P0=
+maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4 h1:zNC9eVAhw8FhKpM3AxNAh/iy75UEYX91uJUvqqAYlvo=
+maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4/go.mod h1:3sOGhXi3P1V6/NruTA0gujkvTypXVUraWktCuTGyDuM=
From bf12524eefee5c83ca2eae8c759715138e0efd09 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 13 May 2026 17:03:48 +0300
Subject: [PATCH 30/94] handlewhatsapp: decrypt message secret data before
rerouting LIDs
---
go.mod | 2 +-
go.sum | 4 +-
pkg/connector/handlewhatsapp.go | 74 +++++++++++++++++----------------
3 files changed, 42 insertions(+), 38 deletions(-)
diff --git a/go.mod b/go.mod
index 033ecf9..9cd6c8f 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
github.com/tidwall/gjson v1.18.0
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260511155711-eb05d94dea7d
+ go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f
golang.org/x/image v0.39.0
golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
diff --git a/go.sum b/go.sum
index 3ea04fb..be22fdf 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,8 @@ go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25 h1:YPEmc+li7TF6C9AdRTcSLMb6y
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260511155711-eb05d94dea7d h1:GBtuMd+MvpxZ0hII0xWzc9N4z1BPhph7aDZb6EizhO4=
-go.mau.fi/whatsmeow v0.0.0-20260511155711-eb05d94dea7d/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
+go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f h1:icWtsD1MH5nlo8mEpHMPZ9+1kgHkjmXQroYi0lHXKZ0=
+go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go
index eb42f43..4c1016c 100644
--- a/pkg/connector/handlewhatsapp.go
+++ b/pkg/connector/handlewhatsapp.go
@@ -309,14 +309,50 @@ func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string,
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
+ }
+ parsedMessageType := getMessageType(evt.Message)
+ if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
+ return
+ }
+ if encReact := evt.Message.GetEncReactionMessage(); encReact != nil {
+ decrypted, err := wa.Client.DecryptReaction(ctx, evt)
+ if err != nil {
+ wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt reaction")
+ return
+ }
+ decrypted.Key = encReact.GetTargetMessageKey()
+ evt.Message.ReactionMessage = decrypted
+ }
+ if encComment := evt.Message.GetEncCommentMessage(); encComment != nil {
+ decrypted, err := wa.Client.DecryptComment(ctx, evt)
+ if err != nil {
+ wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt comment")
+ } else {
+ decrypted.EncCommentMessage = evt.Message.GetEncCommentMessage()
+ evt.Message = decrypted
+ }
+ }
+ if encMessage := evt.Message.GetSecretEncryptedMessage(); encMessage != nil {
+ decrypted, err := wa.Client.DecryptSecretEncryptedMessage(ctx, evt)
+ if err != nil {
+ wa.UserLogin.Log.Err(err).
+ Str("message_id", evt.Info.ID).
+ Stringer("evt_sender", evt.Info.Sender).
+ Any("target_message_key", encMessage.TargetMessageKey).
+ Msg("Failed to decrypt secret-encrypted message")
+ return
+ }
+ evt.RawMessage = decrypted
+ evt.UnwrapRaw()
+ parsedMessageType = getMessageType(evt.Message)
+ }
wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID)
wa.UserLogin.Log.Trace().
Any("info", evt.Info).
Any("payload", evt.Message).
Msg("Received WhatsApp message")
- if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
- return
- }
if evt.Info.IsFromMe &&
evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil &&
wa.Main.Bridge.Config.Backfill.Enabled &&
@@ -351,38 +387,6 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
return
}
- parsedMessageType := getMessageType(evt.Message)
- if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
- return
- }
- if encReact := evt.Message.GetEncReactionMessage(); encReact != nil {
- decrypted, err := wa.Client.DecryptReaction(ctx, evt)
- if err != nil {
- wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt reaction")
- return
- }
- decrypted.Key = encReact.GetTargetMessageKey()
- evt.Message.ReactionMessage = decrypted
- }
- if encComment := evt.Message.GetEncCommentMessage(); encComment != nil {
- decrypted, err := wa.Client.DecryptComment(ctx, evt)
- if err != nil {
- wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt comment")
- } else {
- decrypted.EncCommentMessage = evt.Message.GetEncCommentMessage()
- evt.Message = decrypted
- }
- }
- if encMessage := evt.Message.GetSecretEncryptedMessage(); encMessage != nil {
- decrypted, err := wa.Client.DecryptSecretEncryptedMessage(ctx, evt)
- if err != nil {
- wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt message")
- return
- }
- evt.RawMessage = decrypted
- evt.UnwrapRaw()
- parsedMessageType = getMessageType(evt.Message)
- }
res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{
MessageInfoWrapper: &MessageInfoWrapper{
Info: evt.Info,
From 7f91a71e9d058d2a0cf86e94260b2a117ddbdd65 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 14 May 2026 15:27:58 +0300
Subject: [PATCH 31/94] handlewhatsapp: fix saving history sync notifications
---
pkg/connector/handlewhatsapp.go | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go
index 4c1016c..3abcede 100644
--- a/pkg/connector/handlewhatsapp.go
+++ b/pkg/connector/handlewhatsapp.go
@@ -313,9 +313,6 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
return
}
parsedMessageType := getMessageType(evt.Message)
- if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
- return
- }
if encReact := evt.Message.GetEncReactionMessage(); encReact != nil {
decrypted, err := wa.Client.DecryptReaction(ctx, evt)
if err != nil {
@@ -355,10 +352,12 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
Msg("Received WhatsApp message")
if evt.Info.IsFromMe &&
evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil &&
- wa.Main.Bridge.Config.Backfill.Enabled &&
- wa.Client.ManualHistorySyncDownload {
+ wa.Main.Bridge.Config.Backfill.Enabled {
wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification)
}
+ if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
+ return
+ }
messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation()
if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD {
From 405207108850207c2c7d277832172dae473dd23e Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Sat, 16 May 2026 13:32:45 +0300
Subject: [PATCH 32/94] Bump version to v26.05
---
CHANGELOG.md | 5 ++++
cmd/mautrix-whatsapp/main.go | 2 +-
go.mod | 24 ++++++++++----------
go.sum | 44 ++++++++++++++++++------------------
4 files changed, 40 insertions(+), 35 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 892be2e..5c0314e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+# v26.05
+
+* Added support for importing sticker packs from WhatsApp.
+* Added support for WhatsApp's new message edit encryption scheme.
+
# v26.04
* Added support for @room mentions in both directions.
diff --git a/cmd/mautrix-whatsapp/main.go b/cmd/mautrix-whatsapp/main.go
index b97bc7f..cb1632c 100644
--- a/cmd/mautrix-whatsapp/main.go
+++ b/cmd/mautrix-whatsapp/main.go
@@ -18,7 +18,7 @@ var m = mxmain.BridgeMain{
Name: "mautrix-whatsapp",
URL: "https://github.com/mautrix/whatsapp",
Description: "A Matrix-WhatsApp puppeting bridge.",
- Version: "26.04",
+ Version: "26.05",
SemCalVer: true,
Connector: &connector.WhatsAppConnector{},
}
diff --git a/go.mod b/go.mod
index 9cd6c8f..c52b925 100644
--- a/go.mod
+++ b/go.mod
@@ -2,23 +2,23 @@ module go.mau.fi/mautrix-whatsapp
go 1.25.0
-toolchain go1.26.2
+toolchain go1.26.3
tool go.mau.fi/util/cmd/maubuild
require (
github.com/lib/pq v1.12.3
github.com/rs/zerolog v1.35.1
- github.com/tidwall/gjson v1.18.0
- go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25
+ github.com/tidwall/gjson v1.19.0
+ go.mau.fi/util v0.9.9
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f
- golang.org/x/image v0.39.0
- golang.org/x/net v0.53.0
+ go.mau.fi/whatsmeow v0.0.0-20260516102357-8d3700152a69
+ golang.org/x/image v0.40.0
+ golang.org/x/net v0.54.0
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4
+ maunium.net/go/mautrix v0.28.0
)
require (
@@ -43,11 +43,11 @@ require (
github.com/yuin/goldmark v1.8.2 // indirect
go.mau.fi/libsignal v0.2.1 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
- golang.org/x/crypto v0.50.0 // indirect
- golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
- golang.org/x/mod v0.35.0 // indirect
- golang.org/x/sys v0.43.0 // indirect
- golang.org/x/text v0.36.0 // indirect
+ golang.org/x/crypto v0.51.0 // indirect
+ golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect
+ golang.org/x/mod v0.36.0 // indirect
+ golang.org/x/sys v0.44.0 // indirect
+ golang.org/x/text v0.37.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index be22fdf..5fe1561 100644
--- a/go.sum
+++ b/go.sum
@@ -55,8 +55,8 @@ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDq
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
-github.com/tidwall/gjson v1.18.0/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/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=
@@ -71,31 +71,31 @@ github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
-go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25 h1:YPEmc+li7TF6C9AdRTcSLMb6yCHdF27/wNT7kFLIVNg=
-go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
+go.mau.fi/util v0.9.9 h1:ujDeXCo07HBor5oQLyO1tHklupmqVmPgasc53d7q/NE=
+go.mau.fi/util v0.9.9/go.mod h1:pqt4Vcrt+5gcH/CgrHZg11qSx+b34o6mknGzOEA6waY=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f h1:icWtsD1MH5nlo8mEpHMPZ9+1kgHkjmXQroYi0lHXKZ0=
-go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
+go.mau.fi/whatsmeow v0.0.0-20260516102357-8d3700152a69 h1:rRcG2I1LVQ11kH2+KSYxNeFNZXp1oKA5Yxmagsf5bks=
+go.mau.fi/whatsmeow v0.0.0-20260516102357-8d3700152a69/go.mod h1:SY+3c678dbtckGZF16M+sfEW8ZxTb9xkaKwhXueF5yE=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
-golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
-golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
-golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
-golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
-golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
-golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
-golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
-golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
-golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
-golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
+golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
+golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
+golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
+golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
+golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
+golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
+golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
+golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
+golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
+golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
-golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
-golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
+golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
+golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
+golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
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.27.1-0.20260513120123-5fba7e3afae4 h1:zNC9eVAhw8FhKpM3AxNAh/iy75UEYX91uJUvqqAYlvo=
-maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4/go.mod h1:3sOGhXi3P1V6/NruTA0gujkvTypXVUraWktCuTGyDuM=
+maunium.net/go/mautrix v0.28.0 h1:vBakLzf8MAdfED3NzAKiMeKQbc3AQ4EAS03NC+TVMXQ=
+maunium.net/go/mautrix v0.28.0/go.mod h1:/a9A7LGaqb9B3nho4tLd28n0EPcCdwpm2dxkxkLLgh0=
From cbe182fcb1082d5f24225ee812b39d9c2c14db70 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Tue, 19 May 2026 17:54:05 +0300
Subject: [PATCH 33/94] 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 c52b925..89dc3a7 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.28.0
+ maunium.net/go/mautrix v0.28.1-0.20260519145316-34b5f49408a3
)
require (
diff --git a/go.sum b/go.sum
index 5fe1561..bfe01a5 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.28.0 h1:vBakLzf8MAdfED3NzAKiMeKQbc3AQ4EAS03NC+TVMXQ=
-maunium.net/go/mautrix v0.28.0/go.mod h1:/a9A7LGaqb9B3nho4tLd28n0EPcCdwpm2dxkxkLLgh0=
+maunium.net/go/mautrix v0.28.1-0.20260519145316-34b5f49408a3 h1:l86igJY8Te5JEBNPL9NSpZaGv6eHsxtCrAabXv8KPJo=
+maunium.net/go/mautrix v0.28.1-0.20260519145316-34b5f49408a3/go.mod h1:/a9A7LGaqb9B3nho4tLd28n0EPcCdwpm2dxkxkLLgh0=
From b46b41edf882059131bfcdd4dbecb3ab8ea808d5 Mon Sep 17 00:00:00 2001
From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com>
Date: Tue, 19 May 2026 20:51:26 +0530
Subject: [PATCH 34/94] handlewhatsapp,msgconv,directmedia: fix HD media
replacements (#920)
Co-authored-by: Tulir Asokan
---
pkg/connector/directmedia.go | 4 ++++
pkg/connector/events.go | 3 ++-
pkg/connector/handlewhatsapp.go | 4 ++++
pkg/msgconv/wa-media.go | 9 ++++++++-
pkg/waid/id.go | 7 ++++---
pkg/waid/mediaid.go | 12 ++++++++++--
6 files changed, 32 insertions(+), 7 deletions(-)
diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go
index ac2fbd2..70289cd 100644
--- a/pkg/connector/directmedia.go
+++ b/pkg/connector/directmedia.go
@@ -17,6 +17,7 @@
package connector
import (
+ "bytes"
"context"
"encoding/json"
"errors"
@@ -171,6 +172,9 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
if err != nil {
return nil, fmt.Errorf("failed to unmarshal media keys: %w", err)
}
+ if version := parsedID.Message.Version; len(version) > 0 && !bytes.Equal(version, keys.EncSHA256) && !bytes.Equal(version, keys.SHA256) {
+ return nil, mautrix.MNotFound.WithMessage("Version mismatch, media was likely replaced")
+ }
var ul *bridgev2.UserLogin
if parsedID.UserLogin != "" {
ul = wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin)
diff --git a/pkg/connector/events.go b/pkg/connector/events.go
index de5bdb8..3e809b7 100644
--- a/pkg/connector/events.go
+++ b/pkg/connector/events.go
@@ -91,6 +91,7 @@ type WAMessageEvent struct {
parsedMessageType string
isUndecryptableUpsertSubEvent bool
+ dontRenderEdited bool
postHandle func()
}
@@ -204,7 +205,7 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
}
}
editPart := cm.Parts[0].ToEditPart(existing[0])
- if evt.isUndecryptableUpsertSubEvent {
+ if evt.isUndecryptableUpsertSubEvent || evt.dontRenderEdited {
if editPart.TopLevelExtra == nil {
editPart.TopLevelExtra = make(map[string]any)
}
diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go
index 3abcede..79871b3 100644
--- a/pkg/connector/handlewhatsapp.go
+++ b/pkg/connector/handlewhatsapp.go
@@ -359,6 +359,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
return
}
+ dontRenderEdited := false
messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation()
if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD {
parentKey := messageAssoc.GetParentMessageKey()
@@ -377,6 +378,8 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
evt.Message = &waE2E.Message{
ProtocolMessage: protocolMsg,
}
+ parsedMessageType = getMessageType(evt.Message)
+ dontRenderEdited = true
} else if assocType == waE2E.MessageAssociation_MOTION_PHOTO {
//evt.Message = evt.Message.GetAssociatedChildMessage().GetMessage()
wa.UserLogin.Log.Debug().
@@ -395,6 +398,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
MsgEvent: evt,
parsedMessageType: parsedMessageType,
+ dontRenderEdited: dontRenderEdited,
})
return res.Success
}
diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go
index c2a8624..38fe9e2 100644
--- a/pkg/msgconv/wa-media.go
+++ b/pkg/msgconv/wa-media.go
@@ -90,7 +90,7 @@ 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))
+ preparedMedia.URL, err = portal.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver, getMediaIDVersion(msg)))
if err != nil {
panic(fmt.Errorf("failed to generate content URI: %w", err))
}
@@ -119,6 +119,13 @@ func (mc *MessageConverter) convertMediaMessage(
return
}
+func getMediaIDVersion(msg MediaMessage) []byte {
+ if encSHA256 := msg.GetFileEncSHA256(); len(encSHA256) > 0 {
+ return encSHA256
+ }
+ return msg.GetFileSHA256()
+}
+
func (mc *MessageConverter) convertAlbumMessage(ctx context.Context, msg *waE2E.AlbumMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
parts := make([]string, 0, 2)
if msg.GetExpectedImageCount() > 0 {
diff --git a/pkg/waid/id.go b/pkg/waid/id.go
index 840e0f1..e522f5f 100644
--- a/pkg/waid/id.go
+++ b/pkg/waid/id.go
@@ -88,9 +88,10 @@ func MakeFakeMessageID(chat, sender types.JID, data string) networkid.MessageID
}
type ParsedMessageID struct {
- Chat types.JID
- Sender types.JID
- ID types.MessageID
+ Chat types.JID
+ Sender types.JID
+ ID types.MessageID
+ Version []byte
}
func (pmi *ParsedMessageID) String() networkid.MessageID {
diff --git a/pkg/waid/mediaid.go b/pkg/waid/mediaid.go
index 093ece1..716e187 100644
--- a/pkg/waid/mediaid.go
+++ b/pkg/waid/mediaid.go
@@ -36,7 +36,7 @@ const (
mediaIDTypeStickerPackItem = 252
)
-func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID) networkid.MediaID {
+func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID, version []byte) networkid.MediaID {
compactChat := compactJID(messageInfo.Chat.ToNonAD())
compactSender := compactJID(messageInfo.Sender.ToNonAD())
receiverID := compactJID(ParseUserLoginID(receiver, 0))
@@ -46,7 +46,7 @@ func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, rec
} else {
compactID = compactMsgID(messageInfo.ID)
}
- mediaID := make([]byte, 0, 5+len(compactChat)+len(compactSender)+len(receiverID)+len(compactID))
+ mediaID := make([]byte, 0, 6+len(compactChat)+len(compactSender)+len(receiverID)+len(compactID)+len(version))
mediaID = append(mediaID, mediaIDTypeMessage)
mediaID = append(mediaID, byte(len(compactChat)))
mediaID = append(mediaID, compactChat...)
@@ -56,6 +56,8 @@ func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, rec
mediaID = append(mediaID, receiverID...)
mediaID = append(mediaID, byte(len(compactID)))
mediaID = append(mediaID, compactID...)
+ mediaID = append(mediaID, byte(len(version)))
+ mediaID = append(mediaID, version...)
return mediaID
}
@@ -138,6 +140,12 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) {
Sender: senderJID,
ID: id,
}
+ if len(mediaID) > 0 {
+ parsed.Message.Version, err = readCompact(&mediaID, rawBytes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse version: %w", err)
+ }
+ }
parsed.UserLogin = MakeUserLoginID(receiverID)
case mediaIDTypeAvatar, mediaIDTypeCommunityAvatar:
targetJID, err := readCompact(&mediaID, parseCompactJID)
From c74327e8e48dd5f0be3bd36d32909434822d346b Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 21 May 2026 16:49:45 +0300
Subject: [PATCH 35/94] login: use 400 for whatsapp ratelimits
---
pkg/connector/login.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pkg/connector/login.go b/pkg/connector/login.go
index 88e7b75..b70ab1a 100644
--- a/pkg/connector/login.go
+++ b/pkg/connector/login.go
@@ -76,8 +76,8 @@ var (
}
ErrRateLimitedByWhatsApp = bridgev2.RespError{
ErrCode: "FI.MAU.WHATSAPP.RATE_LIMITED",
- Err: "Rate limited by WhatsApp",
- StatusCode: http.StatusTooManyRequests,
+ Err: "Rate limited by WhatsApp. Try again later.",
+ StatusCode: http.StatusBadRequest,
}
)
From 9e7dc0bef0066ab536af5382ebac8523190d8b45 Mon Sep 17 00:00:00 2001
From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com>
Date: Mon, 25 May 2026 18:10:58 +0530
Subject: [PATCH 36/94] msgconv/from-whatsapp: convert message history notices
(#922)
Co-authored-by: Tulir Asokan
---
pkg/connector/mediarequest.go | 5 +-
pkg/connector/wamsgtype.go | 2 +
pkg/msgconv/from-whatsapp.go | 2 +
pkg/msgconv/wa-misc.go | 87 +++++++++++++++++++++++++++++++++++
pkg/waid/dbmeta.go | 7 +++
5 files changed, 100 insertions(+), 3 deletions(-)
diff --git a/pkg/connector/mediarequest.go b/pkg/connector/mediarequest.go
index 2c382d1..2686250 100644
--- a/pkg/connector/mediarequest.go
+++ b/pkg/connector/mediarequest.go
@@ -70,10 +70,9 @@ func (wa *WhatsAppClient) processFailedMedia(ctx context.Context, portalKey netw
func (wa *WhatsAppClient) mediaRequestLoop(ctx context.Context) {
log := wa.UserLogin.Log.With().Str("loop", "media requests").Logger()
ctx = log.WithContext(ctx)
- tzName := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).Timezone
- userTz, err := time.LoadLocation(tzName)
+ userTz, err := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).LoadTimezone()
var startIn time.Duration
- if tzName != "" && err == nil && userTz != nil {
+ if err == nil && userTz != nil {
now := time.Now()
startAt := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, userTz)
startAt = startAt.Add(time.Duration(wa.Main.Config.HistorySync.MediaRequests.RequestLocalTime) * time.Minute)
diff --git a/pkg/connector/wamsgtype.go b/pkg/connector/wamsgtype.go
index 92e7f86..a5b65e8 100644
--- a/pkg/connector/wamsgtype.go
+++ b/pkg/connector/wamsgtype.go
@@ -130,6 +130,8 @@ func getMessageType(waMsg *waE2E.Message) string {
return "secret encrypted"
case waMsg.PollResultSnapshotMessage != nil:
return "poll result snapshot"
+ case waMsg.MessageHistoryNotice != nil:
+ return "message history notice"
case waMsg.MessageHistoryBundle != nil:
return "message history bundle"
case waMsg.RequestPhoneNumberMessage != nil:
diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go
index 11569e2..f0e4560 100644
--- a/pkg/msgconv/from-whatsapp.go
+++ b/pkg/msgconv/from-whatsapp.go
@@ -215,6 +215,8 @@ func (mc *MessageConverter) ToMatrix(
part, contextInfo = mc.convertPlaceholderMessage(ctx, waMsg)
case waMsg.GroupInviteMessage != nil:
part, contextInfo = mc.convertGroupInviteMessage(ctx, info, waMsg.GroupInviteMessage)
+ case waMsg.MessageHistoryNotice != nil:
+ part, contextInfo = mc.convertMessageHistoryNotice(ctx, info, waMsg.MessageHistoryNotice)
case waMsg.ProtocolMessage != nil && waMsg.ProtocolMessage.GetType() == waE2E.ProtocolMessage_EPHEMERAL_SETTING:
part, contextInfo = mc.convertEphemeralSettingMessage(ctx, waMsg.ProtocolMessage, info.Timestamp, isBackfill)
case waMsg.EncCommentMessage != nil:
diff --git a/pkg/msgconv/wa-misc.go b/pkg/msgconv/wa-misc.go
index 7ca4427..71de586 100644
--- a/pkg/msgconv/wa-misc.go
+++ b/pkg/msgconv/wa-misc.go
@@ -26,6 +26,7 @@ import (
"github.com/rs/zerolog"
"go.mau.fi/util/exerrors"
+ "go.mau.fi/util/exfmt"
"go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow/proto/waAICommonDeprecated"
"go.mau.fi/whatsmeow/proto/waE2E"
@@ -117,6 +118,92 @@ func (mc *MessageConverter) convertGroupInviteMessage(ctx context.Context, info
}, msg.GetContextInfo()
}
+func (mc *MessageConverter) formatMessageHistoryNoticeJID(ctx context.Context, jid types.JID) string {
+ _, displayName, err := mc.getBasicUserInfo(ctx, jid)
+ if err != nil {
+ zerolog.Ctx(ctx).Err(err).Stringer("jid", jid).Msg("Failed to get user info for message history notice")
+ } else if displayName != "" {
+ return displayName
+ }
+ switch jid.Server {
+ case types.DefaultUserServer:
+ return "+" + jid.User
+ default:
+ return "Unknown user " + jid.String()
+ }
+}
+
+const maxMessageHistoryNoticeReceivers = 5
+
+var others = exfmt.Pluralizable("other")
+
+func (mc *MessageConverter) formatMessageHistoryNoticeReceivers(ctx context.Context, receivers []string) string {
+ receiverLimit := min(len(receivers), maxMessageHistoryNoticeReceivers)
+ receiverNames := make([]string, 0, receiverLimit)
+ for _, receiver := range receivers[:receiverLimit] {
+ jid, err := types.ParseJID(receiver)
+ if err != nil {
+ zerolog.Ctx(ctx).Err(err).Str("receiver", receiver).Msg("Failed to parse message history receiver JID")
+ receiverNames = append(receiverNames, receiver)
+ } else {
+ receiverNames = append(receiverNames, mc.formatMessageHistoryNoticeJID(ctx, jid))
+ }
+ }
+ receiverText := strings.Join(receiverNames, ", ")
+ if len(receivers) > receiverLimit {
+ receiverText = fmt.Sprintf("%s + %s", receiverText, others(len(receivers)-receiverLimit))
+ }
+ return receiverText
+}
+
+func (mc *MessageConverter) messageHistoryNoticeLocation(ctx context.Context) *time.Location {
+ portal := getPortal(ctx)
+ loginID := portal.Receiver
+ if loginID == "" {
+ loginID = waid.MakeUserLoginID(getClient(ctx).Store.GetJID().ToNonAD())
+ }
+ if login := mc.Bridge.GetCachedUserLoginByID(loginID); login != nil {
+ meta, _ := login.Metadata.(*waid.UserLoginMetadata)
+ loc, err := meta.LoadTimezone()
+ if err != nil {
+ zerolog.Ctx(ctx).Err(err).Str("timezone", meta.Timezone).Msg("Failed to load user timezone for message history notice")
+ } else if loc != nil {
+ return loc
+ }
+ }
+ return time.Local
+}
+
+func (mc *MessageConverter) convertMessageHistoryNotice(ctx context.Context, info *types.MessageInfo, msg *waE2E.MessageHistoryNotice) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
+ metadata := msg.GetMessageHistoryMetadata()
+
+ sender := mc.formatMessageHistoryNoticeJID(ctx, info.Sender)
+ body := fmt.Sprintf("%s sent message history", sender)
+ if receiverText := mc.formatMessageHistoryNoticeReceivers(ctx, metadata.GetHistoryReceivers()); receiverText != "" {
+ body = fmt.Sprintf("%s to %s", body, receiverText)
+ }
+ if count := metadata.GetMessageCount(); count > 0 {
+ messageWord := "messages"
+ if count == 1 {
+ messageWord = "message"
+ }
+ body = fmt.Sprintf("%s (%d %s)", body, count, messageWord)
+ }
+ if metadata != nil && metadata.OldestMessageTimestampInWindow != nil {
+ oldestTS := time.Unix(metadata.GetOldestMessageTimestampInWindow(), 0).In(mc.messageHistoryNoticeLocation(ctx))
+ body = fmt.Sprintf("%s, starting %s", body, oldestTS.Format("Jan 2, 2006 at 3:04 PM"))
+ }
+ body += "."
+
+ return &bridgev2.ConvertedMessagePart{
+ Type: event.EventMessage,
+ Content: &event.MessageEventContent{
+ MsgType: event.MsgNotice,
+ Body: body,
+ },
+ }, msg.GetContextInfo()
+}
+
func (mc *MessageConverter) convertEphemeralSettingMessage(ctx context.Context, msg *waE2E.ProtocolMessage, ts time.Time, isBackfill bool) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
portal := getPortal(ctx)
portalMeta := portal.Metadata.(*waid.PortalMetadata)
diff --git a/pkg/waid/dbmeta.go b/pkg/waid/dbmeta.go
index 2785105..59e9a9e 100644
--- a/pkg/waid/dbmeta.go
+++ b/pkg/waid/dbmeta.go
@@ -61,6 +61,13 @@ func (m *UserLoginMetadata) GeneratePushKeys() {
}
}
+func (m *UserLoginMetadata) LoadTimezone() (*time.Location, error) {
+ if m == nil || m.Timezone == "" {
+ return nil, nil
+ }
+ return time.LoadLocation(m.Timezone)
+}
+
type MessageErrorType string
const (
From df86e40e1ef60c11b346b76ae7697e98ca185a5e Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Mon, 25 May 2026 15:20:23 +0300
Subject: [PATCH 37/94] config: validate displayname template when loading
config
---
pkg/connector/config.go | 23 ++++++++++++++++++++---
1 file changed, 20 insertions(+), 3 deletions(-)
diff --git a/pkg/connector/config.go b/pkg/connector/config.go
index 2445647..e51c80a 100644
--- a/pkg/connector/config.go
+++ b/pkg/connector/config.go
@@ -2,6 +2,7 @@ package connector
import (
_ "embed"
+ "fmt"
"strings"
"text/template"
"time"
@@ -90,7 +91,15 @@ func (c *Config) UnmarshalYAML(node *yaml.Node) error {
func (c *Config) PostProcess() error {
var err error
c.displaynameTemplate, err = template.New("displayname").Parse(c.DisplaynameTemplate)
- return err
+ if err != nil {
+ return err
+ }
+ // Try to execute template to make sure it's valid
+ _, err = c.formatDisplayname(types.PSAJID, "", types.ContactInfo{})
+ if err != nil {
+ return fmt.Errorf("failed to execute displayname template: %w", err)
+ }
+ return nil
}
func upgradeConfig(helper up.Helper) {
@@ -151,7 +160,7 @@ type DisplaynameParams struct {
Short string
}
-func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.ContactInfo) string {
+func (c *Config) formatDisplayname(jid types.JID, phone string, contact types.ContactInfo) (string, error) {
var nameBuf strings.Builder
if phone == "" && jid.Server == types.DefaultUserServer {
phone = "+" + jid.User
@@ -170,13 +179,21 @@ func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.Co
Name: contact.FullName,
Short: contact.FirstName,
})
+ return nameBuf.String(), err
+}
+
+func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.ContactInfo) string {
+ name, err := c.formatDisplayname(jid, phone, contact)
if err != nil {
panic(err)
}
- return nameBuf.String()
+ return name
}
func redactPhone(phone string) string {
+ if len(phone) <= 4 {
+ return phone
+ }
// This doesn't keep 2+ digit country codes properly, but whatever
return phone[:2] + strings.Repeat("∙", len(phone)-4) + phone[len(phone)-2:]
}
From 30bbbcaf6f351526c9a43beb4d49ece904bf1e95 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Mon, 25 May 2026 15:24:13 +0300
Subject: [PATCH 38/94] msgconv/from-whatsapp: add some html escapes
---
pkg/msgconv/wa-location.go | 3 ++-
pkg/msgconv/wa-misc.go | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/pkg/msgconv/wa-location.go b/pkg/msgconv/wa-location.go
index 17efecd..4a1e043 100644
--- a/pkg/msgconv/wa-location.go
+++ b/pkg/msgconv/wa-location.go
@@ -20,6 +20,7 @@ import (
"bytes"
"context"
"fmt"
+ "html"
"image"
"math"
"net/http"
@@ -51,7 +52,7 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE
MsgType: event.MsgLocation,
Body: fmt.Sprintf("Location: %s\n%s\n%s", name, msg.GetAddress(), url),
Format: event.FormatHTML,
- FormattedBody: fmt.Sprintf("Location: %s
%s", url, name, msg.GetAddress()),
+ 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()),
}
diff --git a/pkg/msgconv/wa-misc.go b/pkg/msgconv/wa-misc.go
index 71de586..fa495e0 100644
--- a/pkg/msgconv/wa-misc.go
+++ b/pkg/msgconv/wa-misc.go
@@ -20,6 +20,7 @@ import (
"context"
"encoding/base64"
"fmt"
+ "html"
"html/template"
"strings"
"time"
@@ -101,7 +102,7 @@ func (mc *MessageConverter) convertGroupInviteMessage(ctx context.Context, info
}
}
- htmlMessage := fmt.Sprintf(template, event.TextToHTML(msg.GetCaption()), msg.GetGroupName(), expiry, mc.Bridge.Config.CommandPrefix)
+ htmlMessage := fmt.Sprintf(template, event.TextToHTML(msg.GetCaption()), html.EscapeString(msg.GetGroupName()), expiry, mc.Bridge.Config.CommandPrefix)
content := &event.MessageEventContent{
MsgType: event.MsgText,
Body: format.HTMLToText(htmlMessage),
From 5e3172b8168454efc1098071a96550a6bdaa968b Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Mon, 25 May 2026 15:33:41 +0300
Subject: [PATCH 39/94] dependencies: update whatsmeow
Closes #918
Fixes #901
---
go.mod | 2 +-
go.sum | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index 89dc3a7..e38d875 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.9
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260516102357-8d3700152a69
+ go.mau.fi/whatsmeow v0.0.0-20260525123251-933deb5f2ee9
golang.org/x/image v0.40.0
golang.org/x/net v0.54.0
golang.org/x/sync v0.20.0
diff --git a/go.sum b/go.sum
index bfe01a5..2782985 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,8 @@ go.mau.fi/util v0.9.9 h1:ujDeXCo07HBor5oQLyO1tHklupmqVmPgasc53d7q/NE=
go.mau.fi/util v0.9.9/go.mod h1:pqt4Vcrt+5gcH/CgrHZg11qSx+b34o6mknGzOEA6waY=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260516102357-8d3700152a69 h1:rRcG2I1LVQ11kH2+KSYxNeFNZXp1oKA5Yxmagsf5bks=
-go.mau.fi/whatsmeow v0.0.0-20260516102357-8d3700152a69/go.mod h1:SY+3c678dbtckGZF16M+sfEW8ZxTb9xkaKwhXueF5yE=
+go.mau.fi/whatsmeow v0.0.0-20260525123251-933deb5f2ee9 h1:l1E9YRXQhjS7Sm6XbYPq8oiSedSCOB6h3FMqZ8pt+K4=
+go.mau.fi/whatsmeow v0.0.0-20260525123251-933deb5f2ee9/go.mod h1:SY+3c678dbtckGZF16M+sfEW8ZxTb9xkaKwhXueF5yE=
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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
From a1f7bf9b9318543f1af0aa6dbbee2b8efe5d7d3e Mon Sep 17 00:00:00 2001
From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com>
Date: Mon, 25 May 2026 19:02:24 +0530
Subject: [PATCH 40/94] msgconv/from-whatsapp: hide message history bundles
(#924)
---
pkg/connector/wamsgtype.go | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/pkg/connector/wamsgtype.go b/pkg/connector/wamsgtype.go
index a5b65e8..4bcf483 100644
--- a/pkg/connector/wamsgtype.go
+++ b/pkg/connector/wamsgtype.go
@@ -132,8 +132,6 @@ func getMessageType(waMsg *waE2E.Message) string {
return "poll result snapshot"
case waMsg.MessageHistoryNotice != nil:
return "message history notice"
- case waMsg.MessageHistoryBundle != nil:
- return "message history bundle"
case waMsg.RequestPhoneNumberMessage != nil:
return "request phone number"
case waMsg.KeepInChatMessage != nil:
@@ -154,7 +152,7 @@ func getMessageType(waMsg *waE2E.Message) string {
return "chat"
case waMsg.PlaceholderMessage != nil:
return "placeholder"
- case waMsg.SenderKeyDistributionMessage != nil, waMsg.StickerSyncRmrMessage != nil:
+ case waMsg.SenderKeyDistributionMessage != nil, waMsg.StickerSyncRmrMessage != nil, waMsg.MessageHistoryBundle != nil:
return "ignore"
default:
return "unknown"
From 91932df97923c57b074f634f527226a7aadac497 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 27 May 2026 14:00:43 +0300
Subject: [PATCH 41/94] 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 e38d875..0a9d8f8 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.9
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260525123251-933deb5f2ee9
+ go.mau.fi/whatsmeow v0.0.0-20260525144132-563bcaa0f632
golang.org/x/image v0.40.0
golang.org/x/net v0.54.0
golang.org/x/sync v0.20.0
diff --git a/go.sum b/go.sum
index 2782985..d86e782 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,8 @@ go.mau.fi/util v0.9.9 h1:ujDeXCo07HBor5oQLyO1tHklupmqVmPgasc53d7q/NE=
go.mau.fi/util v0.9.9/go.mod h1:pqt4Vcrt+5gcH/CgrHZg11qSx+b34o6mknGzOEA6waY=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260525123251-933deb5f2ee9 h1:l1E9YRXQhjS7Sm6XbYPq8oiSedSCOB6h3FMqZ8pt+K4=
-go.mau.fi/whatsmeow v0.0.0-20260525123251-933deb5f2ee9/go.mod h1:SY+3c678dbtckGZF16M+sfEW8ZxTb9xkaKwhXueF5yE=
+go.mau.fi/whatsmeow v0.0.0-20260525144132-563bcaa0f632 h1:ewBVzZW9EBFoxcZJnR8KCYCNXVHH9leNuF6V+pr8Keg=
+go.mau.fi/whatsmeow v0.0.0-20260525144132-563bcaa0f632/go.mod h1:SY+3c678dbtckGZF16M+sfEW8ZxTb9xkaKwhXueF5yE=
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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
From 6c6ea098de74d57dff3e45b15dcfa2f049f76e31 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 27 May 2026 19:23:11 +0300
Subject: [PATCH 42/94] 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 0a9d8f8..f38c6d0 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.28.1-0.20260519145316-34b5f49408a3
+ maunium.net/go/mautrix v0.28.1-0.20260527103108-d00b9b09fcd5
)
require (
diff --git a/go.sum b/go.sum
index d86e782..741ec53 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.28.1-0.20260519145316-34b5f49408a3 h1:l86igJY8Te5JEBNPL9NSpZaGv6eHsxtCrAabXv8KPJo=
-maunium.net/go/mautrix v0.28.1-0.20260519145316-34b5f49408a3/go.mod h1:/a9A7LGaqb9B3nho4tLd28n0EPcCdwpm2dxkxkLLgh0=
+maunium.net/go/mautrix v0.28.1-0.20260527103108-d00b9b09fcd5 h1:9bx9er6TJxQ2saQw63gwT0HZWg9/0Q30x8V3foKuNBo=
+maunium.net/go/mautrix v0.28.1-0.20260527103108-d00b9b09fcd5/go.mod h1:/a9A7LGaqb9B3nho4tLd28n0EPcCdwpm2dxkxkLLgh0=
From 7592f9810377ac5052d321d2cceca2a2dee4fe36 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 28 May 2026 17:22:50 +0300
Subject: [PATCH 43/94] chatinfo: add implicit own membership to parent groups
---
pkg/connector/chatinfo.go | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go
index 5555566..7c7dc8e 100644
--- a/pkg/connector/chatinfo.go
+++ b/pkg/connector/chatinfo.go
@@ -257,7 +257,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
Name: ptr.Ptr(info.Name),
Topic: ptr.Ptr(info.Topic),
Members: &bridgev2.ChatMemberList{
- IsFull: !info.IsIncognito,
+ IsFull: !info.IsIncognito && !info.IsParent,
TotalMemberCount: len(info.Participants),
MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)),
PowerLevels: &bridgev2.PowerLevelOverrides{
@@ -284,11 +284,15 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
},
ExtraUpdates: extraUpdater,
}
+ var hasSelf bool
for _, pcp := range info.Participants {
member := bridgev2.ChatMember{
EventSender: wa.makeEventSender(ctx, pcp.JID),
Membership: event.MembershipJoin,
}
+ if member.EventSender.IsFromMe {
+ hasSelf = true
+ }
if pcp.IsSuperAdmin {
member.PowerLevel = ptr.Ptr(superAdminPL)
} else if pcp.IsAdmin {
@@ -299,18 +303,21 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
member.MemberEventExtra = map[string]any{
"com.beeper.exclude_from_timeline": true,
}
- wrapped.Members.MemberMap[waid.MakeUserID(pcp.JID)] = member
+ wrapped.Members.MemberMap.Set(member)
if pcp.JID.Server == types.HiddenUserServer && !pcp.PhoneNumber.IsEmpty() {
- wrapped.Members.MemberMap[waid.MakeUserID(pcp.PhoneNumber)] = bridgev2.ChatMember{
+ wrapped.Members.MemberMap.Add(bridgev2.ChatMember{
EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(pcp.PhoneNumber)},
Membership: event.MembershipLeave,
PrevMembership: event.MembershipJoin,
MemberEventExtra: map[string]any{
"com.beeper.exclude_from_timeline": true,
},
- }
+ })
}
}
+ if info.IsParent && !hasSelf && info.AddressingMode == types.AddressingModeLID {
+ wrapped.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: wa.makeEventSender(ctx, wa.Device.LID)})
+ }
if !info.LinkedParentJID.IsEmpty() {
wrapped.ParentID = ptr.Ptr(waid.MakePortalID(info.LinkedParentJID))
From eefd547259dd4ec166ab07ca66ec42ad6e131f85 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 29 May 2026 13:24:27 +0300
Subject: [PATCH 44/94] dependencies: update whatsmeow
---
go.mod | 10 +++++-----
go.sum | 20 ++++++++++----------
2 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/go.mod b/go.mod
index f38c6d0..1265cfc 100644
--- a/go.mod
+++ b/go.mod
@@ -12,9 +12,9 @@ require (
github.com/tidwall/gjson v1.19.0
go.mau.fi/util v0.9.9
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260525144132-563bcaa0f632
+ go.mau.fi/whatsmeow v0.0.0-20260529101937-a7ea56383ec4
golang.org/x/image v0.40.0
- golang.org/x/net v0.54.0
+ golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
@@ -41,12 +41,12 @@ require (
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
- go.mau.fi/libsignal v0.2.1 // indirect
+ go.mau.fi/libsignal v0.2.2 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
- golang.org/x/crypto v0.51.0 // indirect
+ golang.org/x/crypto v0.52.0 // indirect
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect
golang.org/x/mod v0.36.0 // indirect
- golang.org/x/sys v0.44.0 // indirect
+ golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
diff --git a/go.sum b/go.sum
index 741ec53..b1c29ff 100644
--- a/go.sum
+++ b/go.sum
@@ -69,31 +69,31 @@ github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTd
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=
-go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
-go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
+go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU=
+go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0=
go.mau.fi/util v0.9.9 h1:ujDeXCo07HBor5oQLyO1tHklupmqVmPgasc53d7q/NE=
go.mau.fi/util v0.9.9/go.mod h1:pqt4Vcrt+5gcH/CgrHZg11qSx+b34o6mknGzOEA6waY=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260525144132-563bcaa0f632 h1:ewBVzZW9EBFoxcZJnR8KCYCNXVHH9leNuF6V+pr8Keg=
-go.mau.fi/whatsmeow v0.0.0-20260525144132-563bcaa0f632/go.mod h1:SY+3c678dbtckGZF16M+sfEW8ZxTb9xkaKwhXueF5yE=
+go.mau.fi/whatsmeow v0.0.0-20260529101937-a7ea56383ec4 h1:PasY/1lMvmSeSxzXLxqgxf95pjwcnTeQ77t4ERiCbY8=
+go.mau.fi/whatsmeow v0.0.0-20260529101937-a7ea56383ec4/go.mod h1:9hto2r5yVE5yyNTRrZErKNSflGBKxIplUVXAD3EJFDE=
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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
-golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
+golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
+golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
-golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
-golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
+golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
+golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
-golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
+golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
From c1ae62ccaed21b5e45013e03be35063b4e4e48c3 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Mon, 1 Jun 2026 01:43:49 +0300
Subject: [PATCH 45/94] 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 1265cfc..47daa19 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.9
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260529101937-a7ea56383ec4
+ go.mau.fi/whatsmeow v0.0.0-20260531220358-9ff5508a26c2
golang.org/x/image v0.40.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
diff --git a/go.sum b/go.sum
index b1c29ff..b4fcfd5 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,8 @@ go.mau.fi/util v0.9.9 h1:ujDeXCo07HBor5oQLyO1tHklupmqVmPgasc53d7q/NE=
go.mau.fi/util v0.9.9/go.mod h1:pqt4Vcrt+5gcH/CgrHZg11qSx+b34o6mknGzOEA6waY=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260529101937-a7ea56383ec4 h1:PasY/1lMvmSeSxzXLxqgxf95pjwcnTeQ77t4ERiCbY8=
-go.mau.fi/whatsmeow v0.0.0-20260529101937-a7ea56383ec4/go.mod h1:9hto2r5yVE5yyNTRrZErKNSflGBKxIplUVXAD3EJFDE=
+go.mau.fi/whatsmeow v0.0.0-20260531220358-9ff5508a26c2 h1:1ymXAy8tRIYzwEL8cmXEWg3QX6XP3hDt2eqBSh2ZtRI=
+go.mau.fi/whatsmeow v0.0.0-20260531220358-9ff5508a26c2/go.mod h1:9hto2r5yVE5yyNTRrZErKNSflGBKxIplUVXAD3EJFDE=
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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
From ccd62ee30e799c0674d7138a1275c2f64152a5ea Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Tue, 2 Jun 2026 20:44:57 +0300
Subject: [PATCH 46/94] backfill: add trace log with content if message fails
to parse
---
go.mod | 2 +-
go.sum | 4 ++--
pkg/connector/backfill.go | 6 ++++++
3 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index 47daa19..4665f82 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.9
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260531220358-9ff5508a26c2
+ go.mau.fi/whatsmeow v0.0.0-20260602173933-595ceb0c22bf
golang.org/x/image v0.40.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
diff --git a/go.sum b/go.sum
index b4fcfd5..b682043 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,8 @@ go.mau.fi/util v0.9.9 h1:ujDeXCo07HBor5oQLyO1tHklupmqVmPgasc53d7q/NE=
go.mau.fi/util v0.9.9/go.mod h1:pqt4Vcrt+5gcH/CgrHZg11qSx+b34o6mknGzOEA6waY=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260531220358-9ff5508a26c2 h1:1ymXAy8tRIYzwEL8cmXEWg3QX6XP3hDt2eqBSh2ZtRI=
-go.mau.fi/whatsmeow v0.0.0-20260531220358-9ff5508a26c2/go.mod h1:9hto2r5yVE5yyNTRrZErKNSflGBKxIplUVXAD3EJFDE=
+go.mau.fi/whatsmeow v0.0.0-20260602173933-595ceb0c22bf h1:zyTgHzoTL1rPeLn8B761tnUoNTHxfhy6DsQbHNzc7/Q=
+go.mau.fi/whatsmeow v0.0.0-20260602173933-595ceb0c22bf/go.mod h1:9hto2r5yVE5yyNTRrZErKNSflGBKxIplUVXAD3EJFDE=
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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go
index b4ef181..0e6de76 100644
--- a/pkg/connector/backfill.go
+++ b/pkg/connector/backfill.go
@@ -260,6 +260,9 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()).
Msg("Dropping historical message due to parse error")
+ log.Trace().
+ Any("web_message_info", rawMsg.GetMessage()).
+ Msg("Content of event that failed to parse")
continue
}
if firstItemTime.IsZero() {
@@ -603,6 +606,9 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
Str("msg_id", msg.GetKey().GetID()).
Uint64("msg_time_seconds", msg.GetMessageTimestamp()).
Msg("Dropping historical message due to parse error")
+ zerolog.Ctx(ctx).Trace().
+ Any("web_message_info", msg.GetMessage()).
+ Msg("Content of event that failed to parse")
continue
}
if !explodeOnError {
From e4f84014e7921783f34f5d88f2a2b8a8bf23c8a9 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 3 Jun 2026 16:32:17 +0300
Subject: [PATCH 47/94] 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 4665f82..4efd3b8 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.9
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260602173933-595ceb0c22bf
+ go.mau.fi/whatsmeow v0.0.0-20260603132417-6a7ac9915382
golang.org/x/image v0.40.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
diff --git a/go.sum b/go.sum
index b682043..764681f 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,8 @@ go.mau.fi/util v0.9.9 h1:ujDeXCo07HBor5oQLyO1tHklupmqVmPgasc53d7q/NE=
go.mau.fi/util v0.9.9/go.mod h1:pqt4Vcrt+5gcH/CgrHZg11qSx+b34o6mknGzOEA6waY=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260602173933-595ceb0c22bf h1:zyTgHzoTL1rPeLn8B761tnUoNTHxfhy6DsQbHNzc7/Q=
-go.mau.fi/whatsmeow v0.0.0-20260602173933-595ceb0c22bf/go.mod h1:9hto2r5yVE5yyNTRrZErKNSflGBKxIplUVXAD3EJFDE=
+go.mau.fi/whatsmeow v0.0.0-20260603132417-6a7ac9915382 h1:j/iSrXBAN0bmQRJysLdnE0hMAqO2ttQTsovO/0nLar4=
+go.mau.fi/whatsmeow v0.0.0-20260603132417-6a7ac9915382/go.mod h1:9hto2r5yVE5yyNTRrZErKNSflGBKxIplUVXAD3EJFDE=
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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
From d9baddaae1f8df98288899038e852f08d2964ee7 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 4 Jun 2026 23:58:09 +0300
Subject: [PATCH 48/94] dependencies: update whatsmeow
---
go.mod | 2 +-
go.sum | 4 ++--
pkg/connector/chatinfo.go | 6 +++---
pkg/connector/directmedia.go | 6 ++----
pkg/connector/userinfo.go | 2 +-
pkg/msgconv/wa-media.go | 4 ++--
6 files changed, 11 insertions(+), 13 deletions(-)
diff --git a/go.mod b/go.mod
index 4efd3b8..ae10fd5 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.9
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260603132417-6a7ac9915382
+ go.mau.fi/whatsmeow v0.0.0-20260604205742-c6a4b703e48f
golang.org/x/image v0.40.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
diff --git a/go.sum b/go.sum
index 764681f..29beb99 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,8 @@ go.mau.fi/util v0.9.9 h1:ujDeXCo07HBor5oQLyO1tHklupmqVmPgasc53d7q/NE=
go.mau.fi/util v0.9.9/go.mod h1:pqt4Vcrt+5gcH/CgrHZg11qSx+b34o6mknGzOEA6waY=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260603132417-6a7ac9915382 h1:j/iSrXBAN0bmQRJysLdnE0hMAqO2ttQTsovO/0nLar4=
-go.mau.fi/whatsmeow v0.0.0-20260603132417-6a7ac9915382/go.mod h1:9hto2r5yVE5yyNTRrZErKNSflGBKxIplUVXAD3EJFDE=
+go.mau.fi/whatsmeow v0.0.0-20260604205742-c6a4b703e48f h1:7JxeZa++EnM3z+94NL+gxnDKWLYyml4W7x2dMMYGUNQ=
+go.mau.fi/whatsmeow v0.0.0-20260604205742-c6a4b703e48f/go.mod h1:9hto2r5yVE5yyNTRrZErKNSflGBKxIplUVXAD3EJFDE=
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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go
index 7c7dc8e..7ffbb14 100644
--- a/pkg/connector/chatinfo.go
+++ b/pkg/connector/chatinfo.go
@@ -457,7 +457,7 @@ func (wa *WhatsAppClient) makePortalAvatarFetcher(avatarID string, sender types.
wrappedAvatar = &bridgev2.Avatar{
ID: networkid.AvatarID(avatar.ID),
Get: func(ctx context.Context) ([]byte, error) {
- return wa.Client.DownloadMediaWithPath(ctx, avatar.DirectPath, nil, nil, nil, 0, "", "")
+ return wa.Client.DownloadMediaWithOnlyPath(ctx, avatar.DirectPath)
},
}
}
@@ -496,7 +496,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
if info.ThreadMeta.Picture != nil {
avatar.ID = networkid.AvatarID(info.ThreadMeta.Picture.ID)
avatar.Get = func(ctx context.Context) ([]byte, error) {
- return wa.Client.DownloadMediaWithPath(ctx, info.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
+ return wa.Client.DownloadMediaWithOnlyPath(ctx, info.ThreadMeta.Picture.DirectPath)
}
} else if info.ThreadMeta.Preview.ID != "" {
avatar.ID = networkid.AvatarID(info.ThreadMeta.Preview.ID)
@@ -507,7 +507,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
} else if meta.ThreadMeta.Picture == nil {
return nil, fmt.Errorf("full res avatar info is missing")
}
- return wa.Client.DownloadMediaWithPath(ctx, meta.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
+ return wa.Client.DownloadMediaWithOnlyPath(ctx, meta.ThreadMeta.Picture.DirectPath)
}
} else {
avatar.ID = "remove"
diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go
index 70289cd..89a54fc 100644
--- a/pkg/connector/directmedia.go
+++ b/pkg/connector/directmedia.go
@@ -131,9 +131,7 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars
}
return &mediaproxy.GetMediaResponseFile{
Callback: func(w *os.File) (*mediaproxy.FileMeta, error) {
- return &mediaproxy.FileMeta{}, waClient.Client.DownloadMediaWithPathToFile(
- ctx, cachedInfo.DirectPath, nil, nil, nil, 0, "", "", w,
- )
+ return &mediaproxy.FileMeta{}, waClient.Client.DownloadMediaWithOnlyPathToFile(ctx, cachedInfo.DirectPath, w)
},
}, nil
}
@@ -227,7 +225,7 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse(
log.Trace().Msg("Retrying download after successful retry")
err = waClient.Client.DownloadToFile(ctx, keys, f)
}
- if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
+ if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
} else if err != nil {
return nil, err
diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go
index aa84938..2b94047 100644
--- a/pkg/connector/userinfo.go
+++ b/pkg/connector/userinfo.go
@@ -357,7 +357,7 @@ func (wa *WhatsAppClient) fetchGhostAvatar(ctx context.Context, ghost *bridgev2.
wrappedAvatar = &bridgev2.Avatar{
ID: networkid.AvatarID(avatar.ID),
Get: func(ctx context.Context) ([]byte, error) {
- return wa.Client.DownloadMediaWithPath(ctx, avatar.DirectPath, nil, nil, nil, 0, "", "")
+ return wa.Client.DownloadMediaWithOnlyPath(ctx, avatar.DirectPath)
},
}
}
diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go
index 38fe9e2..34c529b 100644
--- a/pkg/msgconv/wa-media.go
+++ b/pkg/msgconv/wa-media.go
@@ -374,7 +374,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
var err error
part.URL, part.File, err = intent.UploadMediaStream(ctx, roomID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
err := client.DownloadToFile(ctx, message, file.(*os.File))
- if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
+ if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
} else if err != nil {
return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
@@ -395,7 +395,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
}
} else {
data, err := client.Download(ctx, message)
- if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
+ if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
} else if err != nil {
return fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
From 5e13abdc30419c73250d90f242f5c3ce54be7050 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Tue, 9 Jun 2026 12:25:24 +0300
Subject: [PATCH 49/94] dependencies: update
---
go.mod | 20 ++++++++++----------
go.sum | 36 ++++++++++++++++++------------------
2 files changed, 28 insertions(+), 28 deletions(-)
diff --git a/go.mod b/go.mod
index ae10fd5..f37b568 100644
--- a/go.mod
+++ b/go.mod
@@ -2,7 +2,7 @@ module go.mau.fi/mautrix-whatsapp
go 1.25.0
-toolchain go1.26.3
+toolchain go1.26.4
tool go.mau.fi/util/cmd/maubuild
@@ -12,13 +12,13 @@ require (
github.com/tidwall/gjson v1.19.0
go.mau.fi/util v0.9.9
go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260604205742-c6a4b703e48f
- golang.org/x/image v0.40.0
+ go.mau.fi/whatsmeow v0.0.0-20260609091626-4e622162b959
+ golang.org/x/image v0.42.0
golang.org/x/net v0.55.0
- golang.org/x/sync v0.20.0
+ golang.org/x/sync v0.21.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.28.1-0.20260527103108-d00b9b09fcd5
+ maunium.net/go/mautrix v0.28.1-0.20260609092200-dbbc3d0ed084
)
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.44 // indirect
+ github.com/mattn/go-sqlite3 v1.14.45 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect
@@ -43,11 +43,11 @@ require (
github.com/yuin/goldmark v1.8.2 // indirect
go.mau.fi/libsignal v0.2.2 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
- golang.org/x/crypto v0.52.0 // indirect
- golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect
+ golang.org/x/crypto v0.53.0 // indirect
+ golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect
golang.org/x/mod v0.36.0 // indirect
- golang.org/x/sys v0.45.0 // indirect
- golang.org/x/text v0.37.0 // indirect
+ golang.org/x/sys v0.46.0 // indirect
+ golang.org/x/text v0.38.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 29beb99..589125f 100644
--- a/go.sum
+++ b/go.sum
@@ -34,8 +34,8 @@ 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.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
-github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
+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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -75,27 +75,27 @@ go.mau.fi/util v0.9.9 h1:ujDeXCo07HBor5oQLyO1tHklupmqVmPgasc53d7q/NE=
go.mau.fi/util v0.9.9/go.mod h1:pqt4Vcrt+5gcH/CgrHZg11qSx+b34o6mknGzOEA6waY=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260604205742-c6a4b703e48f h1:7JxeZa++EnM3z+94NL+gxnDKWLYyml4W7x2dMMYGUNQ=
-go.mau.fi/whatsmeow v0.0.0-20260604205742-c6a4b703e48f/go.mod h1:9hto2r5yVE5yyNTRrZErKNSflGBKxIplUVXAD3EJFDE=
+go.mau.fi/whatsmeow v0.0.0-20260609091626-4e622162b959 h1:5MpMyxG2lGLgnN0zKfD6fnDBvyGXoOlruLK34tV281w=
+go.mau.fi/whatsmeow v0.0.0-20260609091626-4e622162b959/go.mod h1:9hto2r5yVE5yyNTRrZErKNSflGBKxIplUVXAD3EJFDE=
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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
-golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
-golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
-golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
-golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
-golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
+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-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q=
+golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
+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.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
-golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
-golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
+golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
-golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
-golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
+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=
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.1-0.20260527103108-d00b9b09fcd5 h1:9bx9er6TJxQ2saQw63gwT0HZWg9/0Q30x8V3foKuNBo=
-maunium.net/go/mautrix v0.28.1-0.20260527103108-d00b9b09fcd5/go.mod h1:/a9A7LGaqb9B3nho4tLd28n0EPcCdwpm2dxkxkLLgh0=
+maunium.net/go/mautrix v0.28.1-0.20260609092200-dbbc3d0ed084 h1:eYPasZ1KwR+8p4nOmafIX4ZAu25lY4ouN99m64qg8iA=
+maunium.net/go/mautrix v0.28.1-0.20260609092200-dbbc3d0ed084/go.mod h1:5rE5/8dcwa7V6BpJ28TDNP676GQHHiTkBtACPbwtdTE=
From 1d68051dae9337a3466de6409b0c8ec86ab73807 Mon Sep 17 00:00:00 2001
From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com>
Date: Tue, 9 Jun 2026 15:25:21 +0530
Subject: [PATCH 50/94] handlewhatsapp: fix HD media caption edits (#927)
---
pkg/connector/events.go | 7 ++++++-
pkg/connector/handlewhatsapp.go | 19 ++++++++++++-------
2 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/pkg/connector/events.go b/pkg/connector/events.go
index 3e809b7..411c39e 100644
--- a/pkg/connector/events.go
+++ b/pkg/connector/events.go
@@ -182,12 +182,15 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
}
var editedMsg *waE2E.Message
var previouslyConvertedPart *bridgev2.ConvertedMessagePart
+ targetMessage := evt.GetTargetMessage()
+ cacheMessage := targetMessage
if evt.isUndecryptableUpsertSubEvent {
// TODO db metadata needs to be updated in this case to remove the error
editedMsg = evt.Message
+ cacheMessage = evt.GetID()
} else {
editedMsg = evt.Message.GetProtocolMessage().GetEditedMessage()
- previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, evt.GetTargetMessage())
+ previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, targetMessage)
meta := existing[0].Metadata.(*waid.MessageMetadata)
if slices.Contains(meta.Edits, evt.Info.ID) {
return nil, fmt.Errorf("%w: edit already handled", bridgev2.ErrIgnoringRemoteEvent)
@@ -203,6 +206,8 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
evt.postHandle = func() {
evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), cm, false)
}
+ } else if len(cm.Parts) > 0 && cacheMessage != "" {
+ evt.wa.Main.AddMediaEditCache(portal, cacheMessage, cm.Parts[0])
}
editPart := cm.Parts[0].ToEditPart(existing[0])
if evt.isUndecryptableUpsertSubEvent || evt.dontRenderEdited {
diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go
index 79871b3..e6437ec 100644
--- a/pkg/connector/handlewhatsapp.go
+++ b/pkg/connector/handlewhatsapp.go
@@ -363,23 +363,28 @@ 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()
- associatedMessage := evt.Message.GetAssociatedChildMessage().GetMessage()
+ protocolMsg := evt.Message.GetProtocolMessage()
+ if protocolMsg.GetType() != waE2E.ProtocolMessage_MESSAGE_EDIT || protocolMsg.GetKey() == nil {
+ protocolMsg = &waE2E.ProtocolMessage{
+ Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(),
+ Key: parentKey,
+ EditedMessage: evt.Message.GetAssociatedChildMessage().GetMessage(),
+ }
+ dontRenderEdited = true
+ } else if child := protocolMsg.GetEditedMessage().GetAssociatedChildMessage().GetMessage(); child != nil {
+ protocolMsg.EditedMessage = child
+ protocolMsg.Key = parentKey
+ }
wa.UserLogin.Log.Debug().
Str("message_id", evt.Info.ID).
Str("parent_id", parentKey.GetID()).
Stringer("assoc_type", assocType).
Msg("Received HD replacement message, converting to edit")
- protocolMsg := &waE2E.ProtocolMessage{
- Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(),
- Key: parentKey,
- EditedMessage: associatedMessage,
- }
evt.Message = &waE2E.Message{
ProtocolMessage: protocolMsg,
}
parsedMessageType = getMessageType(evt.Message)
- dontRenderEdited = true
} else if assocType == waE2E.MessageAssociation_MOTION_PHOTO {
//evt.Message = evt.Message.GetAssociatedChildMessage().GetMessage()
wa.UserLogin.Log.Debug().
From 3cd66ef74a198545a0ba64c869c2e28833d75e66 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Sat, 13 Jun 2026 18:37:41 +0300
Subject: [PATCH 51/94] legacyprovision: remove last parts
---
cmd/mautrix-whatsapp/legacyprovision.go | 153 ------------------------
cmd/mautrix-whatsapp/main.go | 9 --
2 files changed, 162 deletions(-)
delete mode 100644 cmd/mautrix-whatsapp/legacyprovision.go
diff --git a/cmd/mautrix-whatsapp/legacyprovision.go b/cmd/mautrix-whatsapp/legacyprovision.go
deleted file mode 100644
index f0527b2..0000000
--- a/cmd/mautrix-whatsapp/legacyprovision.go
+++ /dev/null
@@ -1,153 +0,0 @@
-package main
-
-import (
- "net/http"
- "strings"
-
- "github.com/rs/zerolog/hlog"
- "go.mau.fi/util/exhttp"
- "go.mau.fi/whatsmeow"
- "go.mau.fi/whatsmeow/appstate"
- "go.mau.fi/whatsmeow/types"
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/bridgev2"
- "maunium.net/go/mautrix/bridgev2/matrix"
- "maunium.net/go/mautrix/id"
-
- "go.mau.fi/mautrix-whatsapp/pkg/connector"
- "go.mau.fi/mautrix-whatsapp/pkg/waid"
-)
-
-type OtherUserInfo struct {
- MXID id.UserID `json:"mxid"`
- JID types.JID `json:"jid"`
- Name string `json:"displayname"`
- Avatar id.ContentURIString `json:"avatar_url"`
-}
-
-type PortalInfo struct {
- RoomID id.RoomID `json:"room_id"`
- OtherUser *OtherUserInfo `json:"other_user,omitempty"`
- GroupInfo *types.GroupInfo `json:"group_info,omitempty"`
- JustCreated bool `json:"just_created"`
-}
-
-type Error struct {
- Success bool `json:"success"`
- Error string `json:"error"`
- ErrCode string `json:"errcode"`
-}
-
-func legacyProvContacts(w http.ResponseWriter, r *http.Request) {
- userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
- if userLogin == nil {
- return
- }
- if contacts, err := userLogin.Client.(*connector.WhatsAppClient).GetStore().Contacts.GetAllContacts(r.Context()); err != nil {
- hlog.FromRequest(r).Err(err).Msg("Failed to fetch all contacts")
- exhttp.WriteJSONResponse(w, http.StatusInternalServerError, Error{
- Error: "Internal server error while fetching contact list",
- ErrCode: "failed to get contacts",
- })
- } else {
- augmentedContacts := map[types.JID]any{}
- for jid, contact := range contacts {
- var avatarURL id.ContentURIString
- if puppet, _ := m.Bridge.GetExistingGhostByID(r.Context(), waid.MakeUserID(jid)); puppet != nil {
- avatarURL = puppet.AvatarMXC
- }
- augmentedContacts[jid] = map[string]interface{}{
- "Found": contact.Found,
- "FirstName": contact.FirstName,
- "FullName": contact.FullName,
- "PushName": contact.PushName,
- "BusinessName": contact.BusinessName,
- "AvatarURL": avatarURL,
- }
- }
- exhttp.WriteJSONResponse(w, http.StatusOK, augmentedContacts)
- }
-}
-
-func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) {
- number := r.PathValue("number")
- userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
- if userLogin == nil {
- return
- }
- startChat := strings.Contains(r.URL.Path, "/v1/pm/")
- resp, err := userLogin.Client.(*connector.WhatsAppClient).ResolveIdentifier(r.Context(), number, startChat)
- if err != nil {
- hlog.FromRequest(r).Warn().Err(err).Str("identifier", number).Msg("Failed to resolve identifier")
- matrix.RespondWithError(w, err, "Internal error resolving identifier")
- return
- }
- var portal *bridgev2.Portal
- if startChat {
- portal, err = m.Bridge.GetPortalByKey(r.Context(), resp.Chat.PortalKey)
- if err != nil {
- hlog.FromRequest(r).Warn().Err(err).Stringer("portal_key", resp.Chat.PortalKey).Msg("Failed to get portal by key")
- matrix.RespondWithError(w, err, "Internal error getting portal by key")
- return
- }
- err = portal.CreateMatrixRoom(r.Context(), userLogin, nil)
- if err != nil {
- hlog.FromRequest(r).Warn().Err(err).Stringer("portal_key", resp.Chat.PortalKey).Msg("Failed to create matrix room for portal")
- matrix.RespondWithError(w, err, "Internal error creating matrix room for portal")
- return
- }
- } else {
- portal, _ = m.Bridge.GetExistingPortalByKey(r.Context(), resp.Chat.PortalKey)
- }
- var roomID id.RoomID
- if portal != nil {
- roomID = portal.MXID
- }
- exhttp.WriteJSONResponse(w, http.StatusOK, PortalInfo{
- RoomID: roomID,
- OtherUser: &OtherUserInfo{
- JID: waid.ParseUserID(resp.UserID),
- MXID: resp.Ghost.Intent.GetMXID(),
- Name: resp.Ghost.Name,
- Avatar: resp.Ghost.AvatarMXC,
- },
- })
-}
-
-func provAppStateDebug(w http.ResponseWriter, r *http.Request) {
- userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
- if userLogin == nil {
- return
- }
- client := userLogin.Client.(*connector.WhatsAppClient)
- if client.Client == nil {
- mautrix.MNotFound.WithMessage("WhatsApp client not connected").Write(w)
- return
- }
- client.Client.AppStateDebugLogs = true
- err := client.Client.FetchAppState(r.Context(), appstate.WAPatchName(r.PathValue("patch")), r.URL.Query().Get("full") == "1", false)
- client.Client.AppStateDebugLogs = false
- if err != nil {
- mautrix.MUnknown.WithMessage("Failed to fetch app state: %v", err).Write(w)
- } else {
- exhttp.WriteEmptyJSONResponse(w, http.StatusOK)
- }
-}
-
-func provRecoverAppStateDebug(w http.ResponseWriter, r *http.Request) {
- userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
- if userLogin == nil {
- return
- }
- client := userLogin.Client.(*connector.WhatsAppClient)
- if client.Client == nil {
- mautrix.MNotFound.WithMessage("WhatsApp client not connected").Write(w)
- return
- }
- resp, err := client.Client.SendPeerMessage(r.Context(), whatsmeow.BuildAppStateRecoveryRequest(appstate.WAPatchName(r.PathValue("patch"))))
- if err != nil {
- mautrix.MUnknown.WithMessage("Failed to send app state recovery request: %v", err).Write(w)
- } else {
- exhttp.WriteJSONResponse(w, http.StatusOK, resp)
- }
-}
diff --git a/cmd/mautrix-whatsapp/main.go b/cmd/mautrix-whatsapp/main.go
index cb1632c..1da86bf 100644
--- a/cmd/mautrix-whatsapp/main.go
+++ b/cmd/mautrix-whatsapp/main.go
@@ -24,15 +24,6 @@ var m = mxmain.BridgeMain{
}
func main() {
- m.PostStart = func() {
- if m.Matrix.Provisioning != nil {
- m.Matrix.Provisioning.Router.HandleFunc("GET /v1/contacts", legacyProvContacts)
- m.Matrix.Provisioning.Router.HandleFunc("GET /v1/resolve_identifier/{number}", legacyProvResolveIdentifier)
- m.Matrix.Provisioning.Router.HandleFunc("POST /v1/pm/{number}", legacyProvResolveIdentifier)
- m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/appstate/{patch}", provAppStateDebug)
- m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/recover-appstate/{patch}", provRecoverAppStateDebug)
- }
- }
m.InitVersion(Tag, Commit, BuildTime)
m.Run()
}
From c97a1c4b4dcc994464745cc7fa232082c28912b5 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Sat, 13 Jun 2026 21:32:17 +0300
Subject: [PATCH 52/94] mclient: add more hooks
---
pkg/connector/handlematrix.go | 55 ++++++++++++++++++++++-----------
pkg/connector/handlewhatsapp.go | 3 +-
pkg/connector/mclient.go | 11 +++++--
3 files changed, 47 insertions(+), 22 deletions(-)
diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go
index b0963ec..299449f 100644
--- a/pkg/connector/handlematrix.go
+++ b/pkg/connector/handlematrix.go
@@ -48,11 +48,12 @@ var (
_ bridgev2.DeleteChatHandlingNetworkAPI = (*WhatsAppClient)(nil)
)
-func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (*bridgev2.MatrixMessageResponse, error) {
+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)
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
if err != nil {
return nil, err
@@ -67,19 +68,21 @@ func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridge
return resp, nil
}
-func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (*bridgev2.MatrixMessageResponse, error) {
+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)
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
}
-func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) {
+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)
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.handleConvertedMatrixMessage(ctx, msg, waMsg, req)
}
@@ -154,7 +157,7 @@ func (wa *WhatsAppClient) PreHandleMatrixReaction(_ context.Context, msg *bridge
}, nil
}
-func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (*database.Reaction, error) {
+func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (result *database.Reaction, retErr error) {
messageID, err := waid.ParseMessageID(msg.TargetMessage.ID)
if err != nil {
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
@@ -171,6 +174,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
SenderTimestampMS: proto.Int64(msg.Event.Timestamp),
},
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
var req whatsmeow.SendRequestExtra
if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(wa.Client, messageID), reactionMsg.ReactionMessage)
@@ -192,7 +196,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
}, err
}
-func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error {
+func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) (retErr error) {
messageID, err := waid.ParseMessageID(msg.TargetReaction.MessageID)
if err != nil {
return fmt.Errorf("failed to parse target message ID: %w", err)
@@ -216,12 +220,13 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b
extra.ID = types.MessageID(msg.InputTransactionID)
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra)
zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response")
return err
}
-func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) error {
+func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) (retErr error) {
log := zerolog.Ctx(ctx)
var editID types.MessageID
@@ -245,6 +250,8 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M
if err != nil {
return fmt.Errorf("failed to convert message: %w", err)
}
+ defer wa.mcTrack(edit, time.Now(), &retErr)
+
convertedEdit := wa.Client.BuildEdit(messageID.Chat, messageID.ID, waMsg)
if edit.OrigSender == nil {
convertedEdit.EditedMessage.Message.ProtocolMessage.TimestampMS = proto.Int64(edit.Event.Timestamp)
@@ -259,7 +266,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M
return err
}
-func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error {
+func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) (retErr error) {
log := zerolog.Ctx(ctx)
messageID, err := waid.ParseMessageID(msg.TargetMessage.ID)
if err != nil {
@@ -271,6 +278,7 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
return fmt.Errorf("failed to parse portal ID: %w", err)
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
revokeMessage := wa.Client.BuildRevoke(messageID.Chat, messageID.Sender, messageID.ID)
extra := whatsmeow.SendRequestExtra{}
@@ -283,7 +291,7 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
return err
}
-func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error {
+func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) {
if !receipt.ReadUpTo.After(receipt.LastRead) {
return nil
}
@@ -322,6 +330,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
}
messagesToRead[key] = append(messagesToRead[key], parsed.ID)
}
+ defer wa.mcTrack(receipt, time.Now(), &retErr)
for messageSender, ids := range messagesToRead {
err = wa.Client.MarkRead(ctx, ids, receipt.Receipt.Timestamp, portalJID, messageSender)
if err != nil {
@@ -331,7 +340,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
return err
}
-func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) error {
+func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) (retErr error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
@@ -352,6 +361,7 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.
return nil
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
if wa.Main.Config.SendPresenceOnTyping {
err = wa.updatePresence(ctx, types.PresenceAvailable)
if err != nil {
@@ -363,7 +373,7 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.
var errUnsupportedDisappearingTimer = bridgev2.WrapErrorInStatus(errors.New("invalid value for disappearing timer")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true)
-func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (bool, error) {
+func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (ok bool, retErr error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
@@ -375,6 +385,7 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
return false, fmt.Errorf("%w (%s)", errUnsupportedDisappearingTimer, msg.Content.Timer.Duration)
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
settingTS := time.UnixMilli(msg.Event.Timestamp)
err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS)
if err != nil {
@@ -391,7 +402,7 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
return true, nil
}
-func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
+func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (result *bridgev2.MatrixMembershipResult, retErr error) {
if msg.Type.IsSelf && msg.OrigSender != nil {
return nil, nil
}
@@ -435,6 +446,7 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target)
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action)
if err != nil {
return nil, err
@@ -450,7 +462,7 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
return &bridgev2.MatrixMembershipResult{RedirectTo: waid.MakeUserID(resp[0].JID)}, nil
}
-func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (bool, error) {
+func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (ok bool, retErr error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
@@ -460,6 +472,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev
return false, fmt.Errorf("cannot set room name for DM")
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name)
if err != nil {
return false, err
@@ -471,7 +484,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev
return true, nil
}
-func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (bool, error) {
+func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (ok bool, retErr error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
@@ -481,6 +494,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge
return false, fmt.Errorf("cannot set room topic for DM")
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
newID := wa.Client.GenerateMessageID()
oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID
err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic)
@@ -495,7 +509,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge
return true, nil
}
-func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (bool, error) {
+func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (ok bool, retErr error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
@@ -518,6 +532,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
@@ -585,7 +600,7 @@ func convertRoomAvatar(data []byte) ([]byte, error) {
return buf.Bytes(), nil
}
-func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMute) error {
+func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMute) (retErr error) {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
@@ -596,14 +611,16 @@ func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMu
if !muted || mutedUntil == event.MutedForever {
muteTS = nil
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.Client.SendAppState(ctx, appstate.BuildMuteAbs(chatJID, muted, muteTS))
}
-func (wa *WhatsAppClient) HandleRoomTag(ctx context.Context, msg *bridgev2.MatrixRoomTag) error {
+func (wa *WhatsAppClient) HandleRoomTag(ctx context.Context, msg *bridgev2.MatrixRoomTag) (retErr error) {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
_, isFavorite := msg.Content.Tags[event.RoomTagFavourite]
return wa.Client.SendAppState(ctx, appstate.BuildPin(chatJID, isFavorite))
}
@@ -635,7 +652,7 @@ func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types.
return lastTS, lastKey, nil
}
-func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.MatrixMarkedUnread) error {
+func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.MatrixMarkedUnread) (retErr error) {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
@@ -644,10 +661,11 @@ func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.
if err != nil {
return err
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.Client.SendAppState(ctx, appstate.BuildMarkChatAsRead(chatJID, msg.Content.Unread, lastTS, lastKey))
}
-func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error {
+func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) (retErr error) {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
@@ -668,5 +686,6 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg
if err != nil {
return err
}
+ defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true))
}
diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go
index e6437ec..30a9cd8 100644
--- a/pkg/connector/handlewhatsapp.go
+++ b/pkg/connector/handlewhatsapp.go
@@ -28,7 +28,6 @@ import (
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/appstate"
"go.mau.fi/whatsmeow/proto/waE2E"
- "go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"maunium.net/go/mautrix/bridgev2"
@@ -77,6 +76,7 @@ func init() {
func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
log := wa.UserLogin.Log
ctx := log.WithContext(wa.Main.Bridge.BackgroundCtx)
+ wa.MC.OnWhatsAppEvent(rawEvt)
success = true
switch evt := rawEvt.(type) {
@@ -168,7 +168,6 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
}()
go wa.syncRemoteProfile(ctx, nil)
}
- wa.MC.OnConnect(store.GetWAVersion()[2], wa.Device.Platform)
case *events.OfflineSyncPreview:
log.Info().
Int("message_count", evt.Messages).
diff --git a/pkg/connector/mclient.go b/pkg/connector/mclient.go
index 0930617..f85545a 100644
--- a/pkg/connector/mclient.go
+++ b/pkg/connector/mclient.go
@@ -21,6 +21,7 @@ import (
"encoding/json"
"time"
+ "go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
@@ -39,14 +40,16 @@ func (wa *WhatsAppClient) initMC() {
}
type mClient = interface {
- OnConnect(version uint32, platform string)
+ OnMatrixEvent(any, time.Duration, error)
+ OnWhatsAppEvent(any)
}
type noopMC struct{}
var noopMCInstance mClient = &noopMC{}
-func (n *noopMC) OnConnect(version uint32, platform string) {}
+func (n *noopMC) OnMatrixEvent(any, time.Duration, error) {}
+func (n *noopMC) OnWhatsAppEvent(any) {}
type mWAClient = interface {
MSend(data []byte)
@@ -79,3 +82,7 @@ func (wa *WhatsAppClient) MSave(s json.RawMessage) {
wa.UserLogin.Log.Err(err).Msg("Failed to save MC data")
}
}
+
+func (wa *WhatsAppClient) mcTrack(evt any, start time.Time, err *error) {
+ wa.MC.OnMatrixEvent(evt, time.Since(start), ptr.Val(err))
+}
From f597fd91f4e53532ccb325f0033565dd7193a985 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Tue, 16 Jun 2026 15:13:03 +0300
Subject: [PATCH 53/94] Bump version to v26.06
---
CHANGELOG.md | 8 ++++++++
cmd/mautrix-whatsapp/main.go | 2 +-
go.mod | 16 ++++++++--------
go.sum | 32 ++++++++++++++++----------------
4 files changed, 33 insertions(+), 25 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c0314e..560e2b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+# v26.06
+
+* Added placeholder for group message history share notices.
+* Fixed community spaces not being bridged properly in some cases.
+* Fixed handling edits of HD media.
+* Fixed duplicate message when a message from another own device is initially
+ undecryptable and later gets resolved.
+
# v26.05
* Added support for importing sticker packs from WhatsApp.
diff --git a/cmd/mautrix-whatsapp/main.go b/cmd/mautrix-whatsapp/main.go
index 1da86bf..07baaec 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.05",
+ Version: "26.06",
SemCalVer: true,
Connector: &connector.WhatsAppConnector{},
}
diff --git a/go.mod b/go.mod
index f37b568..85f5c6c 100644
--- a/go.mod
+++ b/go.mod
@@ -10,21 +10,21 @@ 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.9
- go.mau.fi/webp v0.2.0
- go.mau.fi/whatsmeow v0.0.0-20260609091626-4e622162b959
+ go.mau.fi/util v0.9.10
+ go.mau.fi/webp v0.3.0
+ go.mau.fi/whatsmeow v0.0.0-20260616120636-eaa388b4e537
golang.org/x/image v0.42.0
- golang.org/x/net v0.55.0
+ golang.org/x/net v0.56.0
golang.org/x/sync v0.21.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.28.1-0.20260609092200-dbbc3d0ed084
+ maunium.net/go/mautrix v0.28.1
)
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/beeper/argo-go v1.1.2 // indirect
- github.com/coder/websocket v1.8.14 // indirect
+ 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
@@ -44,8 +44,8 @@ require (
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-20260603202125-055de637280b // indirect
- golang.org/x/mod v0.36.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
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
diff --git a/go.sum b/go.sum
index 589125f..e0a405c 100644
--- a/go.sum
+++ b/go.sum
@@ -8,8 +8,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
-github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
-github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
+github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
+github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -71,24 +71,24 @@ github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/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.9 h1:ujDeXCo07HBor5oQLyO1tHklupmqVmPgasc53d7q/NE=
-go.mau.fi/util v0.9.9/go.mod h1:pqt4Vcrt+5gcH/CgrHZg11qSx+b34o6mknGzOEA6waY=
-go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
-go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
-go.mau.fi/whatsmeow v0.0.0-20260609091626-4e622162b959 h1:5MpMyxG2lGLgnN0zKfD6fnDBvyGXoOlruLK34tV281w=
-go.mau.fi/whatsmeow v0.0.0-20260609091626-4e622162b959/go.mod h1:9hto2r5yVE5yyNTRrZErKNSflGBKxIplUVXAD3EJFDE=
+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-20260616120636-eaa388b4e537 h1:LhxtFShA1NXJWz4yNygwtl3Eycp9XsfMqJeDdcLyKmk=
+go.mau.fi/whatsmeow v0.0.0-20260616120636-eaa388b4e537/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=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
-golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q=
-golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
+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.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
-golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
-golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
-golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
+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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -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.1-0.20260609092200-dbbc3d0ed084 h1:eYPasZ1KwR+8p4nOmafIX4ZAu25lY4ouN99m64qg8iA=
-maunium.net/go/mautrix v0.28.1-0.20260609092200-dbbc3d0ed084/go.mod h1:5rE5/8dcwa7V6BpJ28TDNP676GQHHiTkBtACPbwtdTE=
+maunium.net/go/mautrix v0.28.1 h1:Hic3oDMPbLbQu1fhboTRAKZcORMjzzkjxsa+SGk60b0=
+maunium.net/go/mautrix v0.28.1/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=
From 879fc7cb41b0adb7ce986ee50939554c36bfa6a7 Mon Sep 17 00:00:00 2001
From: Nick Mills-Barrett
Date: Wed, 17 Jun 2026 14:15:56 +0100
Subject: [PATCH 54/94] backfill: skip groups that 404 on info (#929)
---
pkg/connector/backfill.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go
index 0e6de76..ee92ac2 100644
--- a/pkg/connector/backfill.go
+++ b/pkg/connector/backfill.go
@@ -400,8 +400,8 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
return
}
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true)
- if errors.Is(err, whatsmeow.ErrNotInGroup) {
- log.Debug().Stringer("chat_jid", conv.ChatJID).
+ if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) {
+ log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID).
Msg("Skipping creating room because the user is not a participant")
//err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID)
//if err != nil {
From d90c7d94d128466195945ef1a8990be8e3e7759d Mon Sep 17 00:00:00 2001
From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com>
Date: Fri, 19 Jun 2026 03:53:06 +0900
Subject: [PATCH 55/94] msgconv/wa-media: pluralize album media counts (#933)
---
pkg/msgconv/wa-media.go | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go
index 34c529b..9964800 100644
--- a/pkg/msgconv/wa-media.go
+++ b/pkg/msgconv/wa-media.go
@@ -27,6 +27,7 @@ import (
"strings"
"github.com/rs/zerolog"
+ "go.mau.fi/util/exfmt"
"go.mau.fi/util/exmime"
"go.mau.fi/util/exslices"
"go.mau.fi/whatsmeow"
@@ -129,10 +130,10 @@ func getMediaIDVersion(msg MediaMessage) []byte {
func (mc *MessageConverter) convertAlbumMessage(ctx context.Context, msg *waE2E.AlbumMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
parts := make([]string, 0, 2)
if msg.GetExpectedImageCount() > 0 {
- parts = append(parts, fmt.Sprintf("%d images", msg.GetExpectedImageCount()))
+ parts = append(parts, exfmt.Pluralizable("image")(int(msg.GetExpectedImageCount())))
}
if msg.GetExpectedVideoCount() > 0 {
- parts = append(parts, fmt.Sprintf("%d videos", msg.GetExpectedVideoCount()))
+ parts = append(parts, exfmt.Pluralizable("video")(int(msg.GetExpectedVideoCount())))
}
var partDesc string
if len(parts) > 0 {
From 41ce80f58393200e9249d29e700c778302ca2a69 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Mon, 22 Jun 2026 15:46:21 +0300
Subject: [PATCH 56/94] 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 85f5c6c..791f3e7 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-20260616120636-eaa388b4e537
+ go.mau.fi/whatsmeow v0.0.0-20260622124518-e94c58afd43b
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 e0a405c..0112e10 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-20260616120636-eaa388b4e537 h1:LhxtFShA1NXJWz4yNygwtl3Eycp9XsfMqJeDdcLyKmk=
-go.mau.fi/whatsmeow v0.0.0-20260616120636-eaa388b4e537/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U=
+go.mau.fi/whatsmeow v0.0.0-20260622124518-e94c58afd43b h1:SeXzfZd3zFyHg+JEDOb5NZqMJRCRK5NXtXy+qzDJGSs=
+go.mau.fi/whatsmeow v0.0.0-20260622124518-e94c58afd43b/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 80b2c16b3d35aace6af59dbe89413046350c6687 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Mon, 22 Jun 2026 15:46:41 +0300
Subject: [PATCH 57/94] userinfo: log last sync time when enqueuing resync
---
pkg/connector/userinfo.go | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go
index 2b94047..e827955 100644
--- a/pkg/connector/userinfo.go
+++ b/pkg/connector/userinfo.go
@@ -29,7 +29,8 @@ var ResyncLoopInterval = 4 * time.Hour
var ResyncJitterSeconds = 3600
func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
- if ghost.Metadata.(*waid.GhostMetadata).LastSync.Add(ResyncMinInterval).After(time.Now()) {
+ lastSync := ghost.Metadata.(*waid.GhostMetadata).LastSync.Time
+ if lastSync.Add(ResyncMinInterval).After(time.Now()) {
return
}
wa.resyncQueueLock.Lock()
@@ -43,6 +44,7 @@ func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
wa.UserLogin.Log.Debug().
Stringer("jid", jid).
Str("next_resync_in", nextResyncIn).
+ Time("last_ghost_resync", lastSync).
Msg("Enqueued resync for ghost")
}
wa.resyncQueueLock.Unlock()
@@ -50,7 +52,8 @@ func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM bool) {
jid, _ := waid.ParsePortalID(portal.ID)
- if portal.Metadata.(*waid.PortalMetadata).LastSync.Add(ResyncMinInterval).After(time.Now()) {
+ lastSync := portal.Metadata.(*waid.PortalMetadata).LastSync.Time
+ if lastSync.Add(ResyncMinInterval).After(time.Now()) {
return
} else if !allowDM && jid.Server != types.GroupServer {
return
@@ -61,6 +64,7 @@ func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM b
wa.UserLogin.Log.Debug().
Stringer("jid", jid).
Stringer("next_resync_in", time.Until(wa.nextResync)).
+ Time("last_portal_resync", lastSync).
Msg("Enqueued resync for portal")
}
wa.resyncQueueLock.Unlock()
From 0c7f5233c8b4a030e396315b9f5d6cc2d598901c Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Mon, 22 Jun 2026 15:48:40 +0300
Subject: [PATCH 58/94] handlewhatsapp: send analytics for timelock event
---
pkg/connector/handlewhatsapp.go | 14 ++++++++++++++
pkg/connector/phoneping.go | 7 +++++++
pkg/waid/dbmeta.go | 3 ++-
3 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go
index 30a9cd8..2b9b145 100644
--- a/pkg/connector/handlewhatsapp.go
+++ b/pkg/connector/handlewhatsapp.go
@@ -126,6 +126,20 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
success = wa.handleWANewsletterLeave(evt)
case *events.Picture:
success = wa.handleWAPictureUpdate(ctx, evt)
+ case *events.NotifyAccountReachoutTimelock:
+ wa.UserLogin.TrackAnalytics("WhatsApp Account Reachout Timelock", map[string]any{
+ "enforcement_type": evt.EnforcementType,
+ "is_active": evt.IsActive,
+ "time_enforcement_ends": evt.TimeEnforcementEnds.Time,
+ })
+ wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil = evt.TimeEnforcementEnds.Time
+ if wa.UserLogin.BridgeState.GetPrevUnsent().StateEvent == status.StateConnected {
+ wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
+ }
+ err := wa.UserLogin.Save(ctx)
+ if err != nil {
+ log.Err(err).Msg("Failed to save user login metadata after reachout timelock update")
+ }
case *events.AppStateSyncComplete:
wa.handleWAAppStateSyncComplete(ctx, evt)
diff --git a/pkg/connector/phoneping.go b/pkg/connector/phoneping.go
index ce38079..71094c9 100644
--- a/pkg/connector/phoneping.go
+++ b/pkg/connector/phoneping.go
@@ -37,6 +37,13 @@ func (wa *WhatsAppClient) FillBridgeState(state status.BridgeState) status.Bridg
state.Error = WAPhoneOffline
state.UserAction = status.UserActionOpenNative
}
+ rtu := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil
+ if !rtu.IsZero() {
+ if state.Info == nil {
+ state.Info = make(map[string]any)
+ }
+ state.Info["reachout_timelock_until"] = rtu
+ }
return state
}
diff --git a/pkg/waid/dbmeta.go b/pkg/waid/dbmeta.go
index 59e9a9e..1e54426 100644
--- a/pkg/waid/dbmeta.go
+++ b/pkg/waid/dbmeta.go
@@ -41,7 +41,8 @@ type UserLoginMetadata struct {
AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"`
- HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"`
+ HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"`
+ ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitempty"`
MData json.RawMessage `json:"mdata,omitempty"`
}
From d5cf334423689ffb67b1469b4bca6b3b0e61a34a Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Wed, 1 Jul 2026 23:01:34 +0300
Subject: [PATCH 59/94] login: add support for passkeys (#936)
---
go.mod | 4 +-
go.sum | 8 ++--
pkg/connector/login.go | 105 ++++++++++++++++++++++++++++++++++++++---
3 files changed, 105 insertions(+), 12 deletions(-)
diff --git a/go.mod b/go.mod
index 791f3e7..c16824c 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.10
go.mau.fi/webp v0.3.0
- go.mau.fi/whatsmeow v0.0.0-20260622124518-e94c58afd43b
+ go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b
golang.org/x/image v0.42.0
golang.org/x/net v0.56.0
golang.org/x/sync v0.21.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.28.1
+ maunium.net/go/mautrix v0.28.2-0.20260701195654-d33ead4e1d86
)
require (
diff --git a/go.sum b/go.sum
index 0112e10..d38c8cb 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-20260622124518-e94c58afd43b h1:SeXzfZd3zFyHg+JEDOb5NZqMJRCRK5NXtXy+qzDJGSs=
-go.mau.fi/whatsmeow v0.0.0-20260622124518-e94c58afd43b/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U=
+go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b h1:ZUk1ErarDNpnbosXR/MeOz2gkqA4S1bh8zjaSRj7N+Y=
+go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b/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=
@@ -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.1 h1:Hic3oDMPbLbQu1fhboTRAKZcORMjzzkjxsa+SGk60b0=
-maunium.net/go/mautrix v0.28.1/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=
+maunium.net/go/mautrix v0.28.2-0.20260701195654-d33ead4e1d86 h1:OKNg/klCID3MoZHuhpMSQThvWOqKtbCrXC6HjlY4+QA=
+maunium.net/go/mautrix v0.28.2-0.20260701195654-d33ead4e1d86/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=
diff --git a/pkg/connector/login.go b/pkg/connector/login.go
index b70ab1a..5da419f 100644
--- a/pkg/connector/login.go
+++ b/pkg/connector/login.go
@@ -2,6 +2,7 @@ package connector
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"net/http"
@@ -12,6 +13,7 @@ import (
"go.mau.fi/util/exsync"
"go.mau.fi/util/jsontime"
"go.mau.fi/whatsmeow"
+ "go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
waLog "go.mau.fi/whatsmeow/util/log"
"maunium.net/go/mautrix/bridgev2"
@@ -25,6 +27,7 @@ const (
LoginStepIDQR = "fi.mau.whatsapp.login.qr"
LoginStepIDPhoneNumber = "fi.mau.whatsapp.login.phone"
LoginStepIDCode = "fi.mau.whatsapp.login.code"
+ LoginStepIDPasskey = "fi.mau.whatsapp.login.passkey"
LoginStepIDComplete = "fi.mau.whatsapp.login.complete"
LoginFlowIDQR = "qr"
@@ -91,9 +94,11 @@ func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User,
Bool("phone_code", flowID == LoginFlowIDPhone).
Logger(),
- WaitForQRs: exsync.NewEvent(),
- LoginComplete: exsync.NewEvent(),
- Received515: exsync.NewEvent(),
+ WaitForQRs: exsync.NewEvent(),
+ LoginComplete: exsync.NewEvent(),
+ PasskeyRequest: exsync.NewEvent(),
+ PasskeyConfirmation: exsync.NewEvent(),
+ Received515: exsync.NewEvent(),
}, nil
}
@@ -114,6 +119,11 @@ type WALogin struct {
Received515 *exsync.Event
PrevQRIndex atomic.Int32
+ PasskeyRequest *exsync.Event
+ PasskeyRequestData *events.PairPasskeyRequest
+ PasskeyConfirmation *exsync.Event
+ PasskeyConfirmationData *events.PairPasskeyConfirmation
+
Closed atomic.Bool
EventHandlerID uint32
}
@@ -122,6 +132,7 @@ var (
_ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil)
_ bridgev2.LoginProcessUserInput = (*WALogin)(nil)
_ bridgev2.LoginProcessWithOverride = (*WALogin)(nil)
+ _ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil)
)
const LoginConnectWait = 15 * time.Second
@@ -262,6 +273,17 @@ func (wl *WALogin) handleEvent(rawEvt any) {
case *events.ClientOutdated:
wl.Log.Error().Msg("Got client outdated error")
wl.LoginError = ErrLoginClientOutdated
+ case *events.PairPasskeyRequest:
+ wl.PasskeyRequestData = evt
+ wl.PasskeyRequest.Set()
+ return
+ case *events.PairPasskeyConfirmation:
+ wl.PasskeyConfirmationData = evt
+ wl.PasskeyConfirmation.Set()
+ return
+ case *events.PairPasskeyError:
+ wl.Log.Err(evt.Error).Msg("Got passkey error")
+ wl.LoginError = evt.Error
case *events.PairSuccess:
wl.Log.Info().Any("event_data", evt).Msg("Got pair successful event")
wl.LoginSuccess = evt
@@ -289,10 +311,14 @@ func (wl *WALogin) handleEvent(rawEvt any) {
func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
if wl.PhoneCode {
- err := wl.LoginComplete.Wait(ctx)
- if err != nil {
+ select {
+ case <-ctx.Done():
wl.Cancel()
- return nil, err
+ return nil, ctx.Err()
+ case <-wl.PasskeyRequest.GetChan():
+ return wl.makePasskeyStep()
+ case <-wl.LoginComplete.GetChan():
+ // continue
}
} else {
prevIndex := int(wl.PrevQRIndex.Load())
@@ -319,9 +345,16 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
case <-ctx.Done():
wl.Cancel()
return nil, ctx.Err()
+ case <-wl.PasskeyRequest.GetChan():
+ return wl.makePasskeyStep()
case <-wl.LoginComplete.GetChan():
+ // continue
}
}
+ return wl.onLoginComplete(ctx)
+}
+
+func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, error) {
if wl.LoginError != nil {
wl.Log.Debug().Err(wl.LoginError).Msg("Login completed with error")
wl.Cancel()
@@ -372,6 +405,66 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
}, nil
}
+func (wl *WALogin) makePasskeyStep() (*bridgev2.LoginStep, error) {
+ pubkeyData, err := json.Marshal(wl.PasskeyRequestData.PublicKey)
+ if err != nil {
+ wl.Log.Err(err).Msg("Failed to marshal public key data")
+ return nil, err
+ }
+ return &bridgev2.LoginStep{
+ Type: bridgev2.LoginStepTypeWebAuthn,
+ StepID: LoginStepIDPasskey,
+ WebAuthnParams: &bridgev2.LoginWebAuthnParams{
+ URL: "https://web.whatsapp.com",
+ PublicKey: pubkeyData,
+ },
+ }, nil
+}
+
+func (wl *WALogin) SubmitWebAuthnResponse(ctx context.Context, rawResp json.RawMessage) (*bridgev2.LoginStep, error) {
+ var resp types.WebAuthnResponse
+ err := json.Unmarshal(rawResp, &resp)
+ if err != nil {
+ wl.Log.Err(err).Msg("Failed to unmarshal WebAuthn response")
+ wl.Cancel()
+ return nil, err
+ }
+ err = wl.Client.SendPasskeyResponse(ctx, &resp)
+ if err != nil {
+ wl.Log.Err(err).Msg("Failed to send WebAuthn response")
+ wl.Cancel()
+ return nil, err
+ }
+ select {
+ case <-ctx.Done():
+ wl.Cancel()
+ return nil, ctx.Err()
+ case <-wl.PasskeyConfirmation.GetChan():
+ if !wl.PasskeyConfirmationData.SkipHandoffUX {
+ wl.Cancel()
+ return nil, bridgev2.RespError{
+ ErrCode: "FI.MAU.WHATSAPP.NOT_IMPLEMENTED",
+ Err: "Displaying WebAuthn pairing confirmation codes is not yet implemented",
+ StatusCode: http.StatusBadRequest,
+ }
+ }
+ err = wl.Client.SendPasskeyConfirmation(ctx)
+ if err != nil {
+ wl.Log.Err(err).Msg("Failed to send WebAuthn confirmation")
+ wl.Cancel()
+ return nil, err
+ }
+ case <-wl.LoginComplete.GetChan():
+ }
+ select {
+ case <-ctx.Done():
+ wl.Cancel()
+ return nil, ctx.Err()
+ case <-wl.LoginComplete.GetChan():
+ }
+ return wl.onLoginComplete(ctx)
+}
+
func (wl *WALogin) Cancel() {
wl.Closed.Store(true)
wl.Client.RemoveEventHandler(wl.EventHandlerID)
From f3ca7ac1c39401e99ef05112082b1d2bc022df83 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 2 Jul 2026 00:15:15 +0300
Subject: [PATCH 60/94] 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 c16824c..bf909be 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
golang.org/x/sync v0.21.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.28.2-0.20260701195654-d33ead4e1d86
+ maunium.net/go/mautrix v0.28.2-0.20260701211445-bbd547b7edc7
)
require (
diff --git a/go.sum b/go.sum
index d38c8cb..5be58b3 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.28.2-0.20260701195654-d33ead4e1d86 h1:OKNg/klCID3MoZHuhpMSQThvWOqKtbCrXC6HjlY4+QA=
-maunium.net/go/mautrix v0.28.2-0.20260701195654-d33ead4e1d86/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=
+maunium.net/go/mautrix v0.28.2-0.20260701211445-bbd547b7edc7 h1:zqwDd/c02keWWjn5AqafdKnahtGChn595Vuw6af5WdU=
+maunium.net/go/mautrix v0.28.2-0.20260701211445-bbd547b7edc7/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=
From 7753bb3999305ea7c7f1825348a56e4d73f30055 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 2 Jul 2026 15:40:33 +0300
Subject: [PATCH 61/94] 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 bf909be..9dc2e58 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
golang.org/x/sync v0.21.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.28.2-0.20260701211445-bbd547b7edc7
+ maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895
)
require (
diff --git a/go.sum b/go.sum
index 5be58b3..1d4b8af 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.28.2-0.20260701211445-bbd547b7edc7 h1:zqwDd/c02keWWjn5AqafdKnahtGChn595Vuw6af5WdU=
-maunium.net/go/mautrix v0.28.2-0.20260701211445-bbd547b7edc7/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=
+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=
From 8d34f6f6bddfa8dee7a5f8a8af4a3c5a17ada342 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 9 Jul 2026 12:12:18 +0300
Subject: [PATCH 62/94] msgconv/from-whatsapp: use correct message info for
decrypting poll votes
---
pkg/connector/backfill.go | 2 +-
pkg/connector/events.go | 9 +++++----
pkg/connector/handlewhatsapp.go | 6 ++++--
pkg/msgconv/from-whatsapp.go | 3 ++-
pkg/msgconv/wa-poll.go | 8 ++++++--
5 files changed, 18 insertions(+), 10 deletions(-)
diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go
index ee92ac2..dd04696 100644
--- a/pkg/connector/backfill.go
+++ b/pkg/connector/backfill.go
@@ -750,7 +750,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
// TODO use proper intent
intent := wa.Main.Bridge.Bot
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, 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)),
diff --git a/pkg/connector/events.go b/pkg/connector/events.go
index 411c39e..cb51e64 100644
--- a/pkg/connector/events.go
+++ b/pkg/connector/events.go
@@ -52,8 +52,9 @@ func (wa *WhatsAppClient) getPortalKeyByMessageSource(ms types.MessageSource) ne
}
type MessageInfoWrapper struct {
- Info types.MessageInfo
- wa *WhatsAppClient
+ OrigSource types.MessageSource
+ Info types.MessageInfo
+ wa *WhatsAppClient
}
func (evt *MessageInfoWrapper) ShouldCreatePortal() bool {
@@ -200,7 +201,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.isViewOnce(), false, previouslyConvertedPart,
+ ctx, portal, evt.wa.Client, intent, editedMsg, evt.MsgEvent.RawMessage, &evt.Info, &evt.OrigSource, evt.isViewOnce(), false, previouslyConvertedPart,
)
if evt.isUndecryptableUpsertSubEvent && isFailedMedia(cm) {
evt.postHandle = func() {
@@ -285,7 +286,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.isViewOnce(), false, nil,
+ ctx, portal, evt.wa.Client, intent, evt.Message, evt.MsgEvent.RawMessage, &evt.Info, &evt.OrigSource, evt.isViewOnce(), false, nil,
)
if isFailedMedia(converted) {
evt.postHandle = func() {
diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go
index 2b9b145..f12ebad 100644
--- a/pkg/connector/handlewhatsapp.go
+++ b/pkg/connector/handlewhatsapp.go
@@ -358,6 +358,7 @@ 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).
@@ -409,8 +410,9 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{
MessageInfoWrapper: &MessageInfoWrapper{
- Info: evt.Info,
- wa: wa,
+ OrigSource: origSource,
+ Info: evt.Info,
+ wa: wa,
},
Message: evt.Message,
MsgEvent: evt,
diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go
index f0e4560..6eea25f 100644
--- a/pkg/msgconv/from-whatsapp.go
+++ b/pkg/msgconv/from-whatsapp.go
@@ -136,6 +136,7 @@ func (mc *MessageConverter) ToMatrix(
waMsg *waE2E.Message,
rawWaMsg *waE2E.Message,
info *types.MessageInfo,
+ origSource *types.MessageSource,
isViewOnce bool,
isBackfill bool,
previouslyConvertedPart *bridgev2.ConvertedMessagePart,
@@ -176,7 +177,7 @@ func (mc *MessageConverter) ToMatrix(
case waMsg.PollCreationMessageV3 != nil:
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV3)
case waMsg.PollUpdateMessage != nil:
- part, contextInfo = mc.convertPollUpdateMessage(ctx, info, waMsg.PollUpdateMessage)
+ part, contextInfo = mc.convertPollUpdateMessage(ctx, info, origSource, waMsg.PollUpdateMessage)
case waMsg.EventMessage != nil:
part, contextInfo = mc.convertEventMessage(ctx, waMsg.EventMessage)
case waMsg.PinInChatMessage != nil:
diff --git a/pkg/msgconv/wa-poll.go b/pkg/msgconv/wa-poll.go
index 14388be..dfe1a25 100644
--- a/pkg/msgconv/wa-poll.go
+++ b/pkg/msgconv/wa-poll.go
@@ -170,7 +170,7 @@ var failedPollUpdatePart = &bridgev2.ConvertedMessagePart{
DontBridge: true,
}
-func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *types.MessageInfo, msg *waE2E.PollUpdateMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
+func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *types.MessageInfo, origSource *types.MessageSource, 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,8 +181,12 @@ 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: *info,
+ Info: infoForDecrypt,
Message: &waE2E.Message{PollUpdateMessage: msg},
})
if err != nil {
From 942d1b4c0ade8ccc8101b61cc2d9be0a5ec9abcd Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 9 Jul 2026 14:27:54 +0300
Subject: [PATCH 63/94] 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 9dc2e58..35a6655 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-20260630180629-b572e5bcb92b
+ go.mau.fi/whatsmeow v0.0.0-20260709092057-73fe7355f59f
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 1d4b8af..3d0391c 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-20260630180629-b572e5bcb92b h1:ZUk1ErarDNpnbosXR/MeOz2gkqA4S1bh8zjaSRj7N+Y=
-go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U=
+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/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 a1a4cf87b336703e3af9a3e172598a2e7fb04d25 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 9 Jul 2026 14:44:00 +0300
Subject: [PATCH 64/94] msgconv/from-matrix: add hack to use LID for message
secrets in DMs
---
pkg/connector/handlematrix.go | 2 +-
pkg/msgconv/from-matrix.go | 2 +-
pkg/msgconv/matrixpoll.go | 22 ++++++++++++++++------
3 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go
index 299449f..9aea18b 100644
--- a/pkg/connector/handlematrix.go
+++ b/pkg/connector/handlematrix.go
@@ -177,7 +177,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
defer wa.mcTrack(msg, time.Now(), &retErr)
var req whatsmeow.SendRequestExtra
if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
- reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(wa.Client, messageID), reactionMsg.ReactionMessage)
+ 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)
}
diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go
index 93de90c..49aedf8 100644
--- a/pkg/msgconv/from-matrix.go
+++ b/pkg/msgconv/from-matrix.go
@@ -143,7 +143,7 @@ func (mc *MessageConverter) ToWhatsApp(
if err != nil {
return nil, nil, fmt.Errorf("failed to parse message ID: %w", err)
}
- rootMsgInfo := MessageIDToInfo(client, parsedID)
+ rootMsgInfo := MessageIDToInfo(ctx, client, parsedID)
message, err = client.EncryptComment(ctx, rootMsgInfo, message)
if err != nil {
return nil, nil, fmt.Errorf("failed to encrypt comment: %w", err)
diff --git a/pkg/msgconv/matrixpoll.go b/pkg/msgconv/matrixpoll.go
index 0dc3213..b53c22f 100644
--- a/pkg/msgconv/matrixpoll.go
+++ b/pkg/msgconv/matrixpoll.go
@@ -115,7 +115,7 @@ func (mc *MessageConverter) PollVoteToWhatsApp(
zerolog.Ctx(ctx).Err(err).Msg("Failed to parse message ID")
return nil, fmt.Errorf("failed to parse message ID")
}
- pollMsgInfo := MessageIDToInfo(client, parsedMsgID)
+ pollMsgInfo := MessageIDToInfo(ctx, client, parsedMsgID)
pollMsgInfo.Type = "poll"
optionHashes := make([][]byte, 0, len(content.Response.Answers))
if pollMsg.Metadata.(*waid.MessageMetadata).IsMatrixPoll {
@@ -146,13 +146,23 @@ func (mc *MessageConverter) PollVoteToWhatsApp(
return &waE2E.Message{PollUpdateMessage: pollUpdate}, err
}
-func MessageIDToInfo(client *whatsmeow.Client, parsedMsgID *waid.ParsedMessageID) *types.MessageInfo {
+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 {
+ chatLID, _ := client.Store.LIDs.GetLIDForPN(ctx, chat)
+ senderLID, _ := client.Store.LIDs.GetLIDForPN(ctx, sender)
+ if !chatLID.IsEmpty() && !senderLID.IsEmpty() {
+ chat = chatLID
+ sender = senderLID
+ }
+ }
return &types.MessageInfo{
MessageSource: types.MessageSource{
- Chat: parsedMsgID.Chat,
- Sender: parsedMsgID.Sender,
- IsFromMe: parsedMsgID.Sender.User == client.Store.GetLID().User || parsedMsgID.Sender.User == client.Store.GetJID().User,
- IsGroup: parsedMsgID.Chat.Server == types.GroupServer,
+ Chat: chat,
+ Sender: sender,
+ IsFromMe: sender.User == client.Store.GetLID().User || sender.User == client.Store.GetJID().User,
+ IsGroup: chat.Server == types.GroupServer,
},
ID: parsedMsgID.ID,
}
From 5250ddfa7d6b92228aff3f29d18b40795e7709d8 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 10 Jul 2026 18:36:17 +0300
Subject: [PATCH 65/94] 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 66/94] 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 67/94] 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 68/94] 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 69/94] 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 70/94] 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 71/94] 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 72/94] 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 73/94] 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 74/94] 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 75/94] 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 76/94] 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 77/94] 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 78/94] 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 79/94] 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 80/94] 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 81/94] 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 82/94] 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 83/94] 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 84/94] 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 85/94] 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 86/94] 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 87/94] 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 88/94] 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 89/94] 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 90/94] 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 91/94] 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 92/94] 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 93/94] 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 94/94] 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=