From 7254783a32bb3361efc716c9571a2b33cfacd9a1 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 24 Apr 2026 12:46:48 +0300 Subject: [PATCH 01/19] 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 02/19] 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 03/19] 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 04/19] 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 05/19] .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 06/19] 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 07/19] 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 08/19] .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 09/19] 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 10/19] 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 11/19] 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 12/19] 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 13/19] 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 14/19] 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 15/19] 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 16/19] 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 17/19] 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 18/19] 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 19/19] 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 {