1
0
Fork 0
mirror of https://github.com/mautrix/whatsapp.git synced 2026-05-15 10:16:52 -04:00

Compare commits

..

No commits in common. "main" and "v0.2604.0" have entirely different histories.

14 changed files with 184 additions and 697 deletions

View file

@ -11,8 +11,7 @@ type: Bug
### Checklist ### Checklist
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. --> <!-- Both items below are mandatory. Issues not following the rules may be closed without comment. -->
* [ ] 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). * [ ] 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. * [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
* [ ] The bug is still present on the main branch. The `!wa version` command output is: ``

12
go.mod
View file

@ -8,17 +8,16 @@ tool go.mau.fi/util/cmd/maubuild
require ( require (
github.com/lib/pq v1.12.3 github.com/lib/pq v1.12.3
github.com/rs/zerolog v1.35.1 github.com/rs/zerolog v1.35.0
github.com/tidwall/gjson v1.18.0 go.mau.fi/util v0.9.8
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25
go.mau.fi/webp v0.2.0 go.mau.fi/webp v0.2.0
go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f go.mau.fi/whatsmeow v0.0.0-20260416104156-3ff20cd3462a
golang.org/x/image v0.39.0 golang.org/x/image v0.39.0
golang.org/x/net v0.53.0 golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0 golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4 maunium.net/go/mautrix v0.27.0
) )
require ( require (
@ -31,11 +30,12 @@ require (
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // 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.42 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect github.com/rs/xid v1.6.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // 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/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect

20
go.sum
View file

@ -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-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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= 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 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= 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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@ -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/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 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= 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 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 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= 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 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU= 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.8 h1:+/jf8eM2dAT2wx9UidmaneH28r/CSCKCniCyby1qWz8=
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs= 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 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q= 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-20260416104156-3ff20cd3462a h1:/7erOAOkZ5d/k9bghMMQPciR0ypmOsM8wGv7bIwyyZo=
go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg= 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 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= 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 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= 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 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= 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.0 h1:yfEYwoIluVWkofUgbZl9gP4i5nQTF+QNsxtb+r5bKlM=
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4/go.mod h1:3sOGhXi3P1V6/NruTA0gujkvTypXVUraWktCuTGyDuM= maunium.net/go/mautrix v0.27.0/go.mod h1:7QpEQiTy6p4LHkXXaZI+N46tGYy8HMhD0JjzZAFoFWs=

View file

@ -246,7 +246,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
return c.Stringer("chat_jid", jid) return c.Stringer("chat_jid", jid)
}) })
var minTime, maxTime, firstItemTime, lastItemTime time.Time var minTime, maxTime time.Time
var minTimeIndex, maxTimeIndex int var minTimeIndex, maxTimeIndex int
ignoredTypes := 0 ignoredTypes := 0
@ -262,10 +262,6 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Msg("Dropping historical message due to parse error") Msg("Dropping historical message due to parse error")
continue continue
} }
if firstItemTime.IsZero() {
firstItemTime = msgEvt.Info.Timestamp
}
lastItemTime = msgEvt.Info.Timestamp
if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) { if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) {
minTime = msgEvt.Info.Timestamp minTime = msgEvt.Info.Timestamp
minTimeIndex = i minTimeIndex = i
@ -298,9 +294,6 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Int("lowest_time_index", minTimeIndex). Int("lowest_time_index", minTimeIndex).
Time("highest_time", maxTime). Time("highest_time", maxTime).
Int("highest_time_index", maxTimeIndex). 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(). Dict("metadata", zerolog.Dict().
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()). Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()). Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).

View file

@ -19,7 +19,6 @@ var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true, AggressiveUpdateInfo: true,
ImplicitReadReceipts: true, ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{ Provisioning: bridgev2.ProvisioningCapabilities{
ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{ ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true, CreateDM: true,
LookupPhone: true, LookupPhone: true,
@ -52,7 +51,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
} }
func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) { func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) {
return 1, 8 return 1, 7
} }
const WAMaxFileSize = 2000 * 1024 * 1024 const WAMaxFileSize = 2000 * 1024 * 1024
@ -67,7 +66,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
} }
func capID() string { func capID() string {
base := "fi.mau.whatsapp.capabilities.2026_05_12" base := "fi.mau.whatsapp.capabilities.2025_12_15"
if ffmpeg.Supported() { if ffmpeg.Supported() {
return base + "+ffmpeg" return base + "+ffmpeg"
} }
@ -126,10 +125,10 @@ var whatsappCaps = &event.RoomFeatures{
event.CapMsgSticker: { event.CapMsgSticker: {
MimeTypes: map[string]event.CapabilitySupportLevel{ MimeTypes: map[string]event.CapabilitySupportLevel{
"image/webp": event.CapLevelFullySupported, "image/webp": event.CapLevelFullySupported,
// TODO see if sending lottie is possible
//"video/lottie+json": event.CapLevelFullySupported,
"image/png": event.CapLevelPartialSupport, "image/png": event.CapLevelPartialSupport,
"image/jpeg": 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, Caption: event.CapLevelDropped,
MaxSize: WAMaxFileSize, MaxSize: WAMaxFileSize,

View file

@ -38,7 +38,6 @@ import (
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status" "maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-whatsapp/pkg/waid" "go.mau.fi/mautrix-whatsapp/pkg/waid"
) )
@ -129,7 +128,6 @@ var (
_ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.StickerImportingNetworkAPI = (*WhatsAppClient)(nil)
) )
var pushCfg = &bridgev2.PushConfig{ var pushCfg = &bridgev2.PushConfig{
@ -469,12 +467,3 @@ func (wa *WhatsAppClient) updatePresence(ctx context.Context, presence types.Pre
} }
return err 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
}

View file

@ -67,8 +67,6 @@ func (wa *WhatsAppConnector) Download(ctx context.Context, mediaID networkid.Med
return wa.downloadMessageDirectMedia(ctx, parsedID, params) return wa.downloadMessageDirectMedia(ctx, parsedID, params)
} else if parsedID.Avatar != nil { } else if parsedID.Avatar != nil {
return wa.downloadAvatarDirectMedia(ctx, parsedID, params) return wa.downloadAvatarDirectMedia(ctx, parsedID, params)
} else if parsedID.Sticker != nil {
return wa.downloadStickerDirectMedia(ctx, parsedID, params)
} else { } else {
return nil, fmt.Errorf("unexpected media ID parsing result") return nil, fmt.Errorf("unexpected media ID parsing result")
} }
@ -137,25 +135,8 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars
}, nil }, 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) { 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()) msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String())
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get message: %w", err) return nil, fmt.Errorf("failed to get message: %w", err)
@ -193,29 +174,16 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
if waClient.Client == nil { if waClient.Client == nil {
return nil, fmt.Errorf("no WhatsApp client found on login") 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{ return &mediaproxy.GetMediaResponseFile{
Callback: func(f *os.File) (*mediaproxy.FileMeta, error) { Callback: func(f *os.File) (*mediaproxy.FileMeta, error) {
log := zerolog.Ctx(ctx) err := waClient.Client.DownloadToFile(ctx, keys, f)
err := waClient.Client.DownloadToFile(ctx, dm, f) if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent) {
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["fi.mau.whatsapp.reload_media"]
if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") { if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") {
return nil, ErrReloadNeeded return nil, ErrReloadNeeded
} }
log.Trace().Msg("Media not found for direct download, requesting and waiting") log.Trace().Msg("Media not found for direct download, requesting and waiting")
err = waClient.requestAndWaitDirectMedia(ctx, msgID, keys) err = waClient.requestAndWaitDirectMedia(ctx, msg.ID, keys)
if err != nil { if err != nil {
log.Trace().Err(err).Msg("Failed to wait for media for direct download") log.Trace().Err(err).Msg("Failed to wait for media for direct download")
return nil, err return nil, err
@ -229,23 +197,24 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse(
return nil, err return nil, err
} }
if mimeType == "application/was" { mime := keys.MimeType
if mime == "application/was" {
if _, err := f.Seek(0, io.SeekStart); err != nil { if _, err := f.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err) return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err)
} else if zipData, err := io.ReadAll(f); err != nil { } else if zipData, err := io.ReadAll(f); err != nil {
return nil, fmt.Errorf("failed to read sticker zip: %w", err) 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) return nil, fmt.Errorf("failed to extract animated sticker: %w %x", err, zipData)
} else if _, err := f.WriteAt(data, 0); err != nil { } else if _, err := f.WriteAt(data, 0); err != nil {
return nil, fmt.Errorf("failed to write animated sticker to file: %w", err) return nil, fmt.Errorf("failed to write animated sticker to file: %w", err)
} else if err := f.Truncate(int64(len(data))); err != nil { } else if err := f.Truncate(int64(len(data))); err != nil {
return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err) return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err)
} }
mimeType = "video/lottie+json" mime = "video/lottie+json"
} }
return &mediaproxy.FileMeta{ return &mediaproxy.FileMeta{
ContentType: mimeType, ContentType: mime,
}, nil }, nil
}, },
}, nil }, nil

View file

@ -107,7 +107,6 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID) wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID)
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID)) msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2)) 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) resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -309,55 +309,20 @@ func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string,
func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) { func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) {
success = true success = true
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
return
}
parsedMessageType := getMessageType(evt.Message)
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.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID)
wa.UserLogin.Log.Trace(). wa.UserLogin.Log.Trace().
Any("info", evt.Info). Any("info", evt.Info).
Any("payload", evt.Message). Any("payload", evt.Message).
Msg("Received WhatsApp message") Msg("Received WhatsApp message")
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
return
}
if evt.Info.IsFromMe && if evt.Info.IsFromMe &&
evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil && evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil &&
wa.Main.Bridge.Config.Backfill.Enabled { wa.Main.Bridge.Config.Backfill.Enabled &&
wa.Client.ManualHistorySyncDownload {
wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification) wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification)
} }
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
return
}
messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation() messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation()
if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD { if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD {
@ -386,6 +351,38 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
return 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{ res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{
MessageInfoWrapper: &MessageInfoWrapper{ MessageInfoWrapper: &MessageInfoWrapper{
Info: evt.Info, Info: evt.Info,

View file

@ -19,7 +19,6 @@ package msgconv
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -202,7 +201,6 @@ func (mc *MessageConverter) constructMediaMessage(
FileSHA256: uploaded.FileSHA256, FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uploaded.FileLength), FileLength: proto.Uint64(uploaded.FileLength),
URL: proto.String(uploaded.URL), URL: proto.String(uploaded.URL),
IsLottie: proto.Bool(mime == "application/was"),
}, },
} }
case event.MsgAudio: case event.MsgAudio:
@ -484,17 +482,6 @@ func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) {
return webpBuffer.Bytes(), size, nil 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( func (mc *MessageConverter) reuploadFileToWhatsApp(
ctx context.Context, content *event.MessageEventContent, ctx context.Context, content *event.MessageEventContent,
) (*whatsmeow.UploadResponse, []byte, string, error) { ) (*whatsmeow.UploadResponse, []byte, string, error) {
@ -503,25 +490,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
if content.FileName != "" { if content.FileName != "" {
fileName = content.FileName fileName = content.FileName
} }
var data []byte data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
var err error
var sticker *types.StickerPackItem
if sticker, err = mc.getOriginalBridgedSticker(ctx, content.Info.BridgedSticker); err != nil {
zerolog.Ctx(ctx).Warn().Err(err).
Msg("Failed to get original bridged sticker, falling back to downloading from URL")
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
} else if sticker != nil {
if sticker.MimeType == "application/was" {
data, err = getClient(ctx).Download(ctx, sticker)
mime = sticker.MimeType
} else {
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
}
content.Info.Width = sticker.Width
content.Info.Height = sticker.Height
} else {
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
}
if err != nil { if err != nil {
return nil, nil, "", fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err) return nil, nil, "", fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
} }
@ -539,14 +508,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
case event.MessageType(event.EventSticker.Type): case event.MessageType(event.EventSticker.Type):
isSticker = true isSticker = true
mediaType = whatsmeow.MediaImage mediaType = whatsmeow.MediaImage
if mime == "video/lottie+json" { if mime != "image/webp" || content.Info.Width != content.Info.Height {
// 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 var size int
data, size, err = mc.convertToWebP(data) data, size, err = mc.convertToWebP(data)
if err != nil { if err != nil {

View file

@ -17,9 +17,6 @@
package msgconv package msgconv
import ( import (
"sync"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
@ -46,16 +43,12 @@ type MessageConverter struct {
DisableViewOnce bool DisableViewOnce bool
DirectMedia bool DirectMedia bool
OldMediaSuffix string OldMediaSuffix string
stickerPackCache map[string]*types.StickerPack
stickerPackCacheLock sync.Mutex
} }
func New(br *bridgev2.Bridge) *MessageConverter { func New(br *bridgev2.Bridge) *MessageConverter {
mc := &MessageConverter{ mc := &MessageConverter{
Bridge: br, Bridge: br,
MaxFileSize: 50 * 1024 * 1024, MaxFileSize: 50 * 1024 * 1024,
stickerPackCache: make(map[string]*types.StickerPack),
} }
mc.HTMLParser = &format.HTMLParser{ mc.HTMLParser = &format.HTMLParser{
PillConverter: mc.convertPill, PillConverter: mc.convertPill,

View file

@ -17,6 +17,8 @@
package msgconv package msgconv
import ( import (
"archive/zip"
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@ -24,18 +26,21 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strconv"
"strings" "strings"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exmime" "go.mau.fi/util/exmime"
"go.mau.fi/util/exslices" "go.mau.fi/util/exslices"
"go.mau.fi/util/lottie"
"go.mau.fi/util/random"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-whatsapp/pkg/waid" "go.mau.fi/mautrix-whatsapp/pkg/waid"
) )
@ -82,11 +87,11 @@ func (mc *MessageConverter) convertMediaMessage(
MimeType: msg.GetMimetype(), MimeType: msg.GetMimetype(),
} }
if mc.DirectMedia { if mc.DirectMedia {
preparedMedia.FillFileName()
if preparedMedia.Info.MimeType == "application/was" { if preparedMedia.Info.MimeType == "application/was" {
preparedMedia.Info.MimeType = "video/lottie+json" preparedMedia.Info.MimeType = "video/lottie+json"
preparedMedia.FileName = "sticker.json" preparedMedia.FileName = "sticker.json"
} }
preparedMedia.FillFileName()
var err error var err error
portal := getPortal(ctx) portal := getPortal(ctx)
idOverride := getEditTargetID(ctx) idOverride := getEditTargetID(ctx)
@ -193,9 +198,7 @@ type PreparedMedia struct {
} }
func (pm *PreparedMedia) FillFileName() *PreparedMedia { func (pm *PreparedMedia) FillFileName() *PreparedMedia {
if pm.Type == event.EventSticker { if pm.FileName == "" {
pm.FileName = ""
} else if pm.FileName == "" {
pm.FileName = strings.TrimPrefix(string(pm.MsgType), "m.") + exmime.ExtensionFromMimetype(pm.Info.MimeType) pm.FileName = strings.TrimPrefix(string(pm.MsgType), "m.") + exmime.ExtensionFromMimetype(pm.Info.MimeType)
} }
return pm return pm
@ -236,19 +239,6 @@ type MediaMessageWithDuration interface {
const WhatsAppStickerSize = 190 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 { func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
extraInfo := map[string]any{} extraInfo := map[string]any{}
data := &PreparedMedia{ data := &PreparedMedia{
@ -297,7 +287,19 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
case *waE2E.StickerMessage: case *waE2E.StickerMessage:
data.Type = event.EventSticker data.Type = event.EventSticker
data.FileName = "sticker" + exmime.ExtensionFromMimetype(msg.GetMimetype()) data.FileName = "sticker" + exmime.ExtensionFromMimetype(msg.GetMimetype())
fixStickerDimensions(data.Info) if msg.GetMimetype() == "application/was" && data.FileName == "sticker" {
data.FileName = "sticker.json"
}
if data.Info.Width == data.Info.Height {
data.Info.Width = WhatsAppStickerSize
data.Info.Height = WhatsAppStickerSize
} else if data.Info.Width > data.Info.Height {
data.Info.Height /= data.Info.Width / WhatsAppStickerSize
data.Info.Width = WhatsAppStickerSize
} else {
data.Info.Width /= data.Info.Height / WhatsAppStickerSize
data.Info.Height = WhatsAppStickerSize
}
case *waE2E.VideoMessage: case *waE2E.VideoMessage:
data.MsgType = event.MsgVideo data.MsgType = event.MsgVideo
pairedMediaType := msg.GetContextInfo().GetPairedMediaType() pairedMediaType := msg.GetContextInfo().GetPairedMediaType()
@ -357,15 +359,12 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
) error { ) error {
client := getClient(ctx) client := getClient(ctx)
intent := getIntent(ctx) intent := getIntent(ctx)
var roomID id.RoomID portal := getPortal(ctx)
if portal := getPortal(ctx); portal != nil {
roomID = portal.MXID
}
var thumbnailData []byte var thumbnailData []byte
var thumbnailInfo *event.FileInfo var thumbnailInfo *event.FileInfo
if part.Info.Size > uploadFileThreshold { if part.Info.Size > uploadFileThreshold {
var err error var err error
part.URL, part.File, err = intent.UploadMediaStream(ctx, roomID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) { part.URL, part.File, err = intent.UploadMediaStream(ctx, portal.MXID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
err := client.DownloadToFile(ctx, message, file.(*os.File)) err := client.DownloadToFile(ctx, message, file.(*os.File))
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) { if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too") zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
@ -398,14 +397,12 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
if err != nil { if err != nil {
return err return err
} }
} else if part.Type == event.EventSticker && part.Info.MimeType == "image/webp" {
mc.fillWebPStickerInfo(ctx, part, data)
} }
if part.Info.MimeType == "" { if part.Info.MimeType == "" {
part.Info.MimeType = http.DetectContentType(data) part.Info.MimeType = http.DetectContentType(data)
} }
part.FillFileName() part.FillFileName()
part.URL, part.File, err = intent.UploadMedia(ctx, roomID, data, part.FileName, part.Info.MimeType) part.URL, part.File, err = intent.UploadMedia(ctx, portal.MXID, data, part.FileName, part.Info.MimeType)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err) return fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err)
} }
@ -414,7 +411,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
var err error var err error
part.Info.ThumbnailURL, part.Info.ThumbnailFile, err = intent.UploadMedia( part.Info.ThumbnailURL, part.Info.ThumbnailFile, err = intent.UploadMedia(
ctx, ctx,
roomID, portal.MXID,
thumbnailData, thumbnailData,
"thumbnail"+exmime.ExtensionFromMimetype(thumbnailInfo.MimeType), "thumbnail"+exmime.ExtensionFromMimetype(thumbnailInfo.MimeType),
thumbnailInfo.MimeType, thumbnailInfo.MimeType,
@ -428,6 +425,68 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
return nil 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 { func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *PreparedMedia, keys *FailedMediaKeys, err error) *bridgev2.ConvertedMessagePart {
logLevel := zerolog.ErrorLevel logLevel := zerolog.ErrorLevel
var extra map[string]any var extra map[string]any
@ -472,3 +531,28 @@ func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *Pre
} }
return part 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
}

View file

@ -1,455 +0,0 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2026 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package msgconv
import (
"archive/zip"
"bytes"
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/rs/zerolog"
"github.com/tidwall/gjson"
"go.mau.fi/util/exstrings"
"go.mau.fi/util/lottie"
"go.mau.fi/util/random"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)
func (mc *MessageConverter) GetCachedStickerPack(ctx context.Context, client *whatsmeow.Client, packID string) (*types.StickerPack, error) {
mc.stickerPackCacheLock.Lock()
defer mc.stickerPackCacheLock.Unlock()
cached, ok := mc.stickerPackCache[packID]
if ok {
if cached == nil {
return nil, bridgev2.RespError(mautrix.MNotFound.WithMessage("sticker pack not found (cached)"))
}
return cached, nil
}
pack, err := client.FetchStickerPack(ctx, packID)
if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) {
mc.stickerPackCache[packID] = nil
return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
} else if err != nil {
return nil, err
}
mc.stickerPackCache[packID] = pack
if packID != pack.StickerPackID {
mc.stickerPackCache[pack.StickerPackID] = pack
}
return pack, nil
}
func (mc *MessageConverter) GetCachedSticker(ctx context.Context, client *whatsmeow.Client, packID string, hash []byte) (*types.StickerPackItem, error) {
pack, err := mc.GetCachedStickerPack(ctx, client, packID)
if err != nil {
return nil, err
}
for _, sticker := range pack.Stickers {
if bytes.Equal(sticker.FileHash, hash) {
return sticker, nil
}
}
return nil, nil
}
func (mc *MessageConverter) DownloadImagePack(ctx context.Context, userLoginID networkid.UserLoginID, client *whatsmeow.Client, inputURL string) (*bridgev2.ImportedImagePack, error) {
parsedURL, err := url.Parse(inputURL)
if err != nil {
return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
} else if parsedURL.Host != "api.whatsapp.com" && parsedURL.Host != "wa.me" {
return nil, bridgev2.WrapRespErr(fmt.Errorf("invalid host %q", parsedURL.Host), mautrix.MNotFound)
} else if !strings.HasPrefix(parsedURL.Path, "/stickerpack/") {
return nil, bridgev2.WrapRespErr(fmt.Errorf("invalid path %q", parsedURL.Path), mautrix.MNotFound)
}
packName := strings.Split(strings.TrimPrefix(parsedURL.Path, "/stickerpack/"), "/")[0]
if packName == "" {
return nil, bridgev2.WrapRespErr(fmt.Errorf("empty pack name"), mautrix.MNotFound)
}
pack, err := mc.GetCachedStickerPack(ctx, client, packName)
if err != nil {
return nil, err
}
canonicalURL := "https://wa.me/stickerpack/" + pack.StickerPackID
topLevelExtra := map[string]any{
"fi.mau.whatsapp.stickerpack": map[string]any{
"id": pack.StickerPackID,
"name": pack.Name,
"description": pack.Description,
"publisher": pack.Publisher,
"animated": pack.Animated > 0,
"lottie": pack.Lottie > 0,
},
}
content := &event.ImagePackEventContent{
Images: make(map[string]*event.ImagePackImage, len(pack.Stickers)),
Metadata: event.ImagePackMetadata{
DisplayName: pack.Name,
AvatarURL: "",
Usage: []event.ImagePackUsage{event.ImagePackUsageSticker},
Attribution: fmt.Sprintf("By %s on WhatsApp %s", pack.Publisher, canonicalURL),
BridgedPack: &event.BridgedStickerPack{
Network: StickerSourceID,
URL: canonicalURL,
},
},
}
ctx = context.WithValue(ctx, contextKeyClient, client)
ctx = context.WithValue(ctx, contextKeyIntent, mc.Bridge.Bot)
ctx = context.WithValue(ctx, contextKeyPortal, (*bridgev2.Portal)(nil))
for i, sticker := range pack.Stickers {
shortcode := sticker.PreviewWebpID
if shortcode == "" {
shortcode = fmt.Sprintf("%s_img%d", pack.StickerPackID, i+1)
}
body := sticker.AccessibilityText
var emoji string
if len(sticker.Emojis) > 0 {
emoji = sticker.Emojis[0]
if body == "" {
body = strings.Join(sticker.Emojis, " ")
}
}
part := &PreparedMedia{
Type: event.EventSticker,
MessageEventContent: &event.MessageEventContent{
Body: body,
Info: &event.FileInfo{
MimeType: sticker.MimeType,
Width: sticker.Width,
Height: sticker.Height,
Size: int(sticker.FileSize),
BridgedSticker: &event.BridgedSticker{
Network: StickerSourceID,
ID: base64.StdEncoding.EncodeToString(sticker.FileHash),
Emoji: emoji,
PackURL: canonicalURL,
},
},
},
TypeDescription: "sticker",
}
dbKey := database.Key(fmt.Sprintf("stickercache:%x", part.Info.BridgedSticker.ID))
fixStickerDimensions(part.Info)
var packed *event.ImagePackImage
if mc.DirectMedia {
dbKey = ""
if part.Info.MimeType == "application/was" {
part.Info.MimeType = "video/lottie+json"
}
part.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeStickerPackMediaID(pack.StickerPackID, sticker.FileHash, userLoginID))
if err != nil {
panic(fmt.Errorf("failed to generate content URI: %w", err))
}
} else if cached := mc.Bridge.DB.KV.Get(ctx, dbKey); cached != "" {
err = json.Unmarshal([]byte(cached), &packed)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal cached sticker data: %w", err)
}
} else {
err = mc.reuploadWhatsAppAttachment(ctx, sticker, part)
if err != nil {
return nil, fmt.Errorf("failed to reupload sticker %q: %w", sticker.GetDirectPath(), err)
}
}
if packed == nil {
packed = &event.ImagePackImage{
URL: part.URL,
Body: part.Body,
Info: part.Info,
}
if dbKey != "" {
data, _ := json.Marshal(packed)
if data != nil {
mc.Bridge.DB.KV.Set(ctx, dbKey, string(data))
}
}
}
content.Images[shortcode] = packed
}
return &bridgev2.ImportedImagePack{
Content: content,
Extra: topLevelExtra,
Shortcode: pack.StickerPackID,
}, nil
}
type StickerMetadata struct {
StickerPackID string `json:"sticker-pack-id"`
AccessibilityText string `json:"accessibility-text"`
Emojis []string `json:"emojis"`
IsFirstPartySticker int `json:"is-first-party-sticker"`
}
func (sm *StickerMetadata) ToMatrix(content *event.MessageEventContent) {
if sm == nil {
return
}
if sm.StickerPackID != "" && content.Info.BridgedSticker == nil {
content.Info.BridgedSticker = &event.BridgedSticker{
Network: StickerSourceID,
PackURL: StickerPackURLPrefix + sm.StickerPackID,
}
if len(sm.Emojis) > 0 {
content.Info.BridgedSticker.Emoji = sm.Emojis[0]
}
}
if sm.AccessibilityText != "" {
content.Body = sm.AccessibilityText
} else if len(sm.Emojis) > 0 {
content.Body = strings.Join(sm.Emojis, " ")
}
}
const StickerSourceID = "whatsapp"
const StickerPackURLPrefix = "https://wa.me/stickerpack/"
func PackAnimatedSticker(data []byte) ([]byte, error) {
var buf bytes.Buffer
zipWriter := zip.NewWriter(&buf)
f, err := zipWriter.Create("animation/animation.json")
if err != nil {
return nil, fmt.Errorf("failed to create zip entry: %w", err)
}
_, err = f.Write(data)
if err != nil {
return nil, fmt.Errorf("failed to write zip entry: %w", err)
}
err = zipWriter.Close()
if err != nil {
return nil, fmt.Errorf("failed to close zip writer: %w", err)
}
return buf.Bytes(), nil
}
func ExtractAnimatedSticker(data []byte) ([]byte, *StickerMetadata, error) {
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil, nil, fmt.Errorf("failed to read sticker zip: %w", err)
}
animationFile, err := zipReader.Open("animation/animation.json")
if err != nil {
return nil, nil, fmt.Errorf("failed to open animation.json: %w", err)
}
animationFileInfo, err := animationFile.Stat()
if err != nil {
_ = animationFile.Close()
return nil, nil, fmt.Errorf("failed to stat animation.json: %w", err)
} else if animationFileInfo.Size() > uploadFileThreshold {
_ = animationFile.Close()
return nil, nil, fmt.Errorf("animation.json is too large (%.2f MiB)", float64(animationFileInfo.Size())/1024/1024)
}
data, err = io.ReadAll(animationFile)
_ = animationFile.Close()
if err != nil {
return nil, nil, fmt.Errorf("failed to read animation.json: %w", err)
}
var meta StickerMetadata
metaFile, err := zipReader.Open("animation/animation.json.overridden_metadata")
if err == nil {
_ = json.NewDecoder(metaFile).Decode(&meta)
_ = metaFile.Close()
}
if meta.StickerPackID == "" {
res := gjson.GetBytes(data, "metadata.customProps")
if res.IsObject() {
_ = json.Unmarshal(exstrings.UnsafeBytes(res.Raw), &meta)
}
}
return data, &meta, nil
}
func (mc *MessageConverter) extractAnimatedSticker(fileInfo *PreparedMedia, data []byte) ([]byte, error) {
data, meta, err := ExtractAnimatedSticker(data)
if err != nil {
return nil, err
}
meta.ToMatrix(fileInfo.MessageEventContent)
fileInfo.Info.MimeType = "video/lottie+json"
fileInfo.FileName = "sticker.json"
return data, nil
}
func (mc *MessageConverter) convertAnimatedSticker(ctx context.Context, fileInfo *PreparedMedia, data []byte) ([]byte, []byte, *event.FileInfo, error) {
data, err := mc.extractAnimatedSticker(fileInfo, data)
if err != nil {
return nil, nil, nil, err
}
c := mc.AnimatedStickerConfig
if c.Target == "disable" {
return data, nil, nil, nil
} else if !lottie.Supported() {
zerolog.Ctx(ctx).Warn().Msg("Animated sticker conversion is enabled, but lottieconverter is not installed")
return data, nil, nil, nil
}
input := bytes.NewReader(data)
fileInfo.Info.MimeType = "image/" + c.Target
fileInfo.FileName = "sticker." + c.Target
switch c.Target {
case "png":
var output bytes.Buffer
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, "1")
return output.Bytes(), nil, nil, err
case "gif":
var output bytes.Buffer
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS))
return output.Bytes(), nil, nil, err
case "webm", "webp":
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("mautrix-whatsapp-lottieconverter-%s.%s", random.String(10), c.Target))
defer func() {
_ = os.Remove(tmpFile)
}()
thumbnailData, err := lottie.FFmpegConvert(ctx, input, tmpFile, c.Args.Width, c.Args.Height, c.Args.FPS)
if err != nil {
return nil, nil, nil, err
}
data, err = os.ReadFile(tmpFile)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to read converted file: %w", err)
}
var thumbnailInfo *event.FileInfo
if thumbnailData != nil {
thumbnailInfo = &event.FileInfo{
MimeType: "image/png",
Width: c.Args.Width,
Height: c.Args.Height,
Size: len(thumbnailData),
}
}
return data, thumbnailData, thumbnailInfo, nil
default:
return nil, nil, nil, fmt.Errorf("unsupported target format %s", c.Target)
}
}
func (mc *MessageConverter) fillWebPStickerInfo(ctx context.Context, fileInfo *PreparedMedia, data []byte) {
meta, err := extractWebPStickerMetadata(data)
if err != nil {
zerolog.Ctx(ctx).Debug().Err(err).Msg("Failed to extract webp sticker metadata")
return
}
meta.ToMatrix(fileInfo.MessageEventContent)
}
// stickerMetadataEXIFTag is the custom EXIF tag WhatsApp uses to embed
// sticker pack metadata as a JSON object inside non-animated webp stickers.
const stickerMetadataEXIFTag = 0x5741
// extractWebPStickerMetadata parses the WhatsApp sticker pack metadata JSON
// embedded in EXIF tag 0x5741 of a non-animated webp sticker.
func extractWebPStickerMetadata(data []byte) (*StickerMetadata, error) {
exif, err := findWebPChunk(data, "EXIF")
if err != nil {
return nil, err
}
raw, err := findEXIFTagValue(exif, stickerMetadataEXIFTag)
if err != nil {
return nil, err
}
var meta StickerMetadata
err = json.Unmarshal(raw, &meta)
if err != nil {
return nil, fmt.Errorf("failed to parse sticker metadata JSON: %w", err)
}
return &meta, nil
}
func findWebPChunk(data []byte, chunkType string) ([]byte, error) {
if len(data) < 12 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WEBP" {
return nil, fmt.Errorf("not a webp file")
}
for pos := 12; pos+8 <= len(data); {
size := binary.LittleEndian.Uint32(data[pos+4 : pos+8])
start := pos + 8
end := start + int(size)
if end > len(data) {
return nil, fmt.Errorf("webp chunk %q extends past end of file", data[pos:pos+4])
}
if string(data[pos:pos+4]) == chunkType {
return data[start:end], nil
}
pos = end
if pos%2 != 0 {
pos++
}
}
return nil, fmt.Errorf("webp chunk %q not found", chunkType)
}
func findEXIFTagValue(exif []byte, tag uint16) ([]byte, error) {
if len(exif) < 8 {
return nil, fmt.Errorf("exif data too short")
}
var bo binary.ByteOrder
switch string(exif[0:2]) {
case "II":
bo = binary.LittleEndian
case "MM":
bo = binary.BigEndian
default:
return nil, fmt.Errorf("invalid TIFF byte order %q", exif[0:2])
}
if bo.Uint16(exif[2:4]) != 0x002A {
return nil, fmt.Errorf("invalid TIFF magic")
}
ifdOffset := int(bo.Uint32(exif[4:8]))
if ifdOffset < 0 || ifdOffset+2 > len(exif) {
return nil, fmt.Errorf("IFD offset out of range")
}
count := int(bo.Uint16(exif[ifdOffset : ifdOffset+2]))
entries := ifdOffset + 2
if entries+count*12 > len(exif) {
return nil, fmt.Errorf("IFD entries out of range")
}
for i := 0; i < count; i++ {
entry := exif[entries+i*12 : entries+(i+1)*12]
if bo.Uint16(entry[0:2]) != tag {
continue
}
// Tag 0x5741 stores JSON as type 7 (UNDEFINED), where size == count bytes.
size := int(bo.Uint32(entry[4:8]))
if size <= 4 {
return entry[8 : 8+size], nil
}
offset := int(bo.Uint32(entry[8:12]))
if offset+size > len(exif) {
return nil, fmt.Errorf("exif tag value out of range")
}
return exif[offset : offset+size], nil
}
return nil, fmt.Errorf("exif tag 0x%04x not found", tag)
}

View file

@ -33,7 +33,6 @@ const (
mediaIDTypeMessage = 255 mediaIDTypeMessage = 255
mediaIDTypeAvatar = 254 mediaIDTypeAvatar = 254
mediaIDTypeCommunityAvatar = 253 mediaIDTypeCommunityAvatar = 253
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) networkid.MediaID {
@ -83,28 +82,9 @@ type AvatarMediaInfo struct {
Community bool 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 { type ParsedMediaID struct {
Message *ParsedMessageID Message *ParsedMessageID
Avatar *AvatarMediaInfo Avatar *AvatarMediaInfo
Sticker *StickerPackMediaInfo
UserLogin networkid.UserLoginID UserLogin networkid.UserLoginID
} }
@ -158,24 +138,6 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) {
Community: mediaIDType == mediaIDTypeCommunityAvatar, Community: mediaIDType == mediaIDTypeCommunityAvatar,
} }
parsed.UserLogin = MakeUserLoginID(receiverID) 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: default:
return nil, fmt.Errorf("unknown media ID type %d", mediaIDType) return nil, fmt.Errorf("unknown media ID type %d", mediaIDType)
} }
@ -284,10 +246,6 @@ func parseCompactJID(jid []byte) (types.JID, error) {
} }
} }
func rawBytes(data []byte) ([]byte, error) {
return data, nil
}
func readCompact[T any](data *networkid.MediaID, fn func(data []byte) (T, error)) (T, error) { func readCompact[T any](data *networkid.MediaID, fn func(data []byte) (T, error)) (T, error) {
var defVal T var defVal T
if len(*data) < 1 { if len(*data) < 1 {