diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 5e6c2b64..992aa1f8 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -682,6 +682,33 @@ export const makeMessagesSocket = (config: SocketConfig) => { return false } + const canonicalizeCarouselRecipients = async (recipientJids: string[]) => { + const uniqueRecipients = [...new Set(recipientJids)] + const pnRecipients = uniqueRecipients.filter(jid => isPnUser(jid) || isHostedPnUser(jid)) + const lidMappings = pnRecipients.length > 0 + ? await signalRepository.lidMapping.getLIDsForPNs(pnRecipients) + : [] + + const lidByPn = new Map((lidMappings || []).map(({ pn, lid }) => [pn, lid])) + const canonicalRecipients: string[] = [] + + for (const jid of uniqueRecipients) { + const canonicalJid = lidByPn.get(jid) || jid + if (!canonicalRecipients.includes(canonicalJid)) { + canonicalRecipients.push(canonicalJid) + } + } + + if (canonicalRecipients.length !== uniqueRecipients.length || canonicalRecipients.some((jid, index) => jid !== uniqueRecipients[index])) { + logger.debug( + { before: uniqueRecipients, after: canonicalRecipients }, + '[CAROUSEL] Canonicalized recipient addressing for session reuse' + ) + } + + return canonicalRecipients + } + const relayMessage = async ( jid: string, message: proto.IMessage, @@ -968,21 +995,30 @@ export const makeMessagesSocket = (config: SocketConfig) => { allRecipients.push(jid) } - await assertSessions(allRecipients) + const isCarouselFanout = isCarouselMessage(message) + const effectiveMeRecipients = isCarouselFanout + ? await canonicalizeCarouselRecipients(meRecipients) + : meRecipients + const effectiveOtherRecipients = isCarouselFanout + ? await canonicalizeCarouselRecipients(otherRecipients) + : otherRecipients + const effectiveAllRecipients = [...effectiveMeRecipients, ...effectiveOtherRecipients] + + await assertSessions(effectiveAllRecipients) const [ { nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 } ] = await Promise.all([ // For own devices: use DSM if available (1:1 chats only) - createParticipantNodes(meRecipients, meMsg || message, extraAttrs), - createParticipantNodes(otherRecipients, message, extraAttrs, meMsg) + createParticipantNodes(effectiveMeRecipients, meMsg || message, extraAttrs), + createParticipantNodes(effectiveOtherRecipients, message, extraAttrs, meMsg) ]) participants.push(...meNodes) participants.push(...otherNodes) - if (meRecipients.length > 0 || otherRecipients.length > 0) { - extraAttrs['phash'] = generateParticipantHashV2([...meRecipients, ...otherRecipients]) + if (effectiveMeRecipients.length > 0 || effectiveOtherRecipients.length > 0) { + extraAttrs['phash'] = generateParticipantHashV2([...effectiveMeRecipients, ...effectiveOtherRecipients]) } shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2 diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index 006dbaae..468498df 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -41,10 +41,12 @@ import type { ILogger } from './logger' import { downloadContentFromMessage, encryptedStream, + extractImageThumb, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData, + getStream, type MediaDownloadOptions } from './messages-media' import { shouldIncludeReportingToken } from './reporting-utils' @@ -473,6 +475,48 @@ export const formatNativeFlowButton = (button: NativeButton): NativeFlowButton = } } +const recoverCarouselImageMetadata = async ( + card: CarouselMessageOptions['cards'][number], + imageMessage: proto.Message.IImageMessage, + mediaOptions: MessageContentGenerationOptions +) => { + if (imageMessage.jpegThumbnail && imageMessage.height && imageMessage.width) { + return + } + + try { + const { stream } = await getStream(card.image!, mediaOptions.options) + const thumb = await extractImageThumb(stream) + + if (!imageMessage.jpegThumbnail) { + imageMessage.jpegThumbnail = thumb.buffer + } + + if (!imageMessage.width && thumb.original.width) { + imageMessage.width = thumb.original.width + } + + if (!imageMessage.height && thumb.original.height) { + imageMessage.height = thumb.original.height + } + + mediaOptions.logger?.info( + { + cardTitle: card.title, + hasJpegThumbnail: !!imageMessage.jpegThumbnail, + width: imageMessage.width, + height: imageMessage.height + }, + '[CAROUSEL] Recovered image thumbnail/dimensions from source media' + ) + } catch (error) { + mediaOptions.logger?.warn( + { cardTitle: card.title, error }, + '[CAROUSEL] Failed to recover image thumbnail/dimensions from source media' + ) + } +} + /** * Generates a button message using Native Flow format wrapped in viewOnceMessage * This is the modern approach for button messages that works on iOS and Android @@ -648,7 +692,10 @@ export const generateCarouselMessage = async ( if (hasMedia && mediaOptions) { if (card.image) { const { imageMessage } = await prepareWAMessageMedia({ image: card.image }, mediaOptions) - // Validate image fields needed for WhatsApp rendering + if (imageMessage) { + await recoverCarouselImageMetadata(card, imageMessage, mediaOptions) + } + if (imageMessage && !imageMessage.jpegThumbnail) { mediaOptions.logger?.warn( { cardTitle: card.title },