Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions whatsapp-bridge/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,16 @@ func extractMediaInfo(msg *waProto.Message, msgTimestamp time.Time, msgID string
doc.GetURL(), doc.GetMediaKey(), doc.GetFileSHA256(), doc.GetFileEncSHA256(), doc.GetFileLength()
}

// Sticker message: WebP image, no caption, same URL+MediaKey+SHA shape as other media.
// On the wire stickers surface as type="media" with an <enc mediatype="sticker"> payload, e.g.:
// <message id="..." type="media">
// <enc mediatype="sticker" type="msg" v="2"><!-- 660 bytes --></enc>
// </message>
if stk := msg.GetStickerMessage(); stk != nil {
return "sticker", "sticker_" + suffix + ".webp",
stk.GetURL(), stk.GetMediaKey(), stk.GetFileSHA256(), stk.GetFileEncSHA256(), stk.GetFileLength()
}

return "", "", "", nil, nil, nil, 0
}

Expand Down Expand Up @@ -1669,6 +1679,8 @@ func downloadMedia(client *whatsmeow.Client, messageStore *MessageStore, message
ext = ".mp4"
case "audio":
ext = ".ogg"
case "sticker":
ext = ".webp"
case "document":
ext = ""
default:
Expand Down Expand Up @@ -1721,6 +1733,10 @@ func downloadMedia(client *whatsmeow.Client, messageStore *MessageStore, message
waMediaType = whatsmeow.MediaAudio
case "document":
waMediaType = whatsmeow.MediaDocument
case "sticker":
// whatsmeow derives sticker decryption keys from the image HKDF info string
// (see download.go: classToMediaType maps "StickerMessage" -> MediaImage).
waMediaType = whatsmeow.MediaImage
default:
return false, "", "", "", fmt.Errorf("unsupported media type: %s", mediaType)
}
Expand Down
55 changes: 55 additions & 0 deletions whatsapp-bridge/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,61 @@ func TestExtractTextContent_SurfacesMediaCaptions(t *testing.T) {
}
}

func TestExtractMediaInfo_Sticker(t *testing.T) {
ts := time.Unix(1710000000, 0).UTC()
msgID := "TEST_STICKER_ID"

url := "https://mmg.whatsapp.net/v/t62.7161-24/sticker.enc"
mediaKey := []byte{0x01, 0x02, 0x03}
sha := []byte{0xaa, 0xbb}
encSha := []byte{0xcc, 0xdd}
var length uint64 = 660

msg := &waProto.Message{
StickerMessage: &waProto.StickerMessage{
URL: proto.String(url),
MediaKey: mediaKey,
FileSHA256: sha,
FileEncSHA256: encSha,
FileLength: proto.Uint64(length),
},
}

gotType, gotFile, gotURL, gotKey, gotSHA, gotEncSHA, gotLen := extractMediaInfo(msg, ts, msgID)

if gotType != "sticker" {
t.Errorf("mediaType = %q, want %q", gotType, "sticker")
}
wantFile := "sticker_" + ts.Format("20060102_150405") + "_" + msgID + ".webp"
if gotFile != wantFile {
t.Errorf("filename = %q, want %q", gotFile, wantFile)
}
if gotURL != url {
t.Errorf("url = %q, want %q", gotURL, url)
}
if string(gotKey) != string(mediaKey) {
t.Errorf("mediaKey = %x, want %x", gotKey, mediaKey)
}
if string(gotSHA) != string(sha) {
t.Errorf("fileSHA256 = %x, want %x", gotSHA, sha)
}
if string(gotEncSHA) != string(encSha) {
t.Errorf("fileEncSHA256 = %x, want %x", gotEncSHA, encSha)
}
if gotLen != length {
t.Errorf("fileLength = %d, want %d", gotLen, length)
}
}

func TestExtractMediaInfo_NoMediaReturnsEmpty(t *testing.T) {
msg := &waProto.Message{Conversation: proto.String("plain text, not media")}
gotType, gotFile, gotURL, gotKey, _, _, gotLen := extractMediaInfo(msg, time.Unix(1710000000, 0), "X")
if gotType != "" || gotFile != "" || gotURL != "" || gotKey != nil || gotLen != 0 {
t.Errorf("non-media should return empty: type=%q file=%q url=%q keyLen=%d len=%d",
gotType, gotFile, gotURL, len(gotKey), gotLen)
}
}

func TestMigrateLegacyLIDChatsToPhoneJIDs_AggregatesByPhoneJIDDeterministically(t *testing.T) {
ms := newTestMessageStore(t)
logger := testLogger()
Expand Down