Compare commits

...

3 Commits

Author SHA1 Message Date
Renato Alcara e152aee740 Fix/pn lid session reuse (#294)
* fix: unify PN and LID session reuse for 1:1 sends

* fix: limit PN/LID reuse changes to session cache
2026-03-16 23:22:05 -03:00
Renato Alcara ff9b84c561 fix: unify PN and LID session reuse for 1:1 sends (#293)
fix: unify PN and LID session reuse for 1:1 sends (#293)
2026-03-16 22:59:28 -03:00
Renato Alcara 50c13398c4 fix: harden carousel live rendering path (#292)
* fix: harden carousel live rendering path

* fix: flatten carousel thumbnail fallback
2026-03-16 22:09:14 -03:00
2 changed files with 102 additions and 6 deletions
+54 -5
View File
@@ -417,6 +417,39 @@ export const makeMessagesSocket = (config: SocketConfig) => {
) )
} }
const resolveSessionJid = async (jid: string) => {
if (isLidUser(jid) || isHostedLidUser(jid)) {
return jid
}
if (isPnUser(jid) || isHostedPnUser(jid)) {
return await signalRepository.lidMapping.getLIDForPN(jid) || jid
}
return jid
}
const canonicalizeSessionRecipients = async (recipientJids: string[]) => {
const uniqueRecipients = [...new Set(recipientJids)]
const canonicalRecipients: string[] = []
for (const jid of uniqueRecipients) {
const canonicalJid = await resolveSessionJid(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 },
'[SESSION] Canonicalized recipient addressing for session reuse'
)
}
return canonicalRecipients
}
const assertSessions = async (jids: string[], force?: boolean) => { const assertSessions = async (jids: string[], force?: boolean) => {
let didFetchNewSession = false let didFetchNewSession = false
const uniqueJids = [...new Set(jids)] // Deduplicate JIDs const uniqueJids = [...new Set(jids)] // Deduplicate JIDs
@@ -426,22 +459,32 @@ export const makeMessagesSocket = (config: SocketConfig) => {
// Check peerSessionsCache and validate sessions using libsignal loadSession // Check peerSessionsCache and validate sessions using libsignal loadSession
for (const jid of uniqueJids) { for (const jid of uniqueJids) {
const signalId = signalRepository.jidToSignalProtocolAddress(jid) const canonicalJid = await resolveSessionJid(jid)
const cachedSession = peerSessionsCache.get(signalId) const signalIds = [
signalRepository.jidToSignalProtocolAddress(jid),
signalRepository.jidToSignalProtocolAddress(canonicalJid)
]
const cachedSession = signalIds
.map(signalId => peerSessionsCache.get(signalId))
.find(session => session !== undefined)
if (cachedSession !== undefined) { if (cachedSession !== undefined) {
if (cachedSession && !force) { if (cachedSession && !force) {
continue // Session exists in cache continue // Session exists in cache
} }
} else { } else {
const sessionValidation = await signalRepository.validateSession(jid) const sessionValidation = await signalRepository.validateSession(canonicalJid)
const hasSession = sessionValidation.exists const hasSession = sessionValidation.exists
peerSessionsCache.set(signalId, hasSession) for (const signalId of signalIds) {
peerSessionsCache.set(signalId, hasSession)
}
if (hasSession && !force) { if (hasSession && !force) {
continue continue
} }
} }
jidsRequiringFetch.push(jid) jidsRequiringFetch.push(canonicalJid)
} }
if (jidsRequiringFetch.length) { if (jidsRequiringFetch.length) {
@@ -968,7 +1011,13 @@ export const makeMessagesSocket = (config: SocketConfig) => {
allRecipients.push(jid) allRecipients.push(jid)
} }
await assertSessions(allRecipients) await assertSessions(allRecipients)
const effectiveMeRecipients = await canonicalizeSessionRecipients(meRecipients)
const effectiveOtherRecipients = await canonicalizeSessionRecipients(otherRecipients)
const effectiveAllRecipients = [...effectiveMeRecipients, ...effectiveOtherRecipients]
await assertSessions(effectiveAllRecipients)
const [ const [
{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: meNodes, shouldIncludeDeviceIdentity: s1 },
+48 -1
View File
@@ -41,10 +41,12 @@ import type { ILogger } from './logger'
import { import {
downloadContentFromMessage, downloadContentFromMessage,
encryptedStream, encryptedStream,
extractImageThumb,
generateThumbnail, generateThumbnail,
getAudioDuration, getAudioDuration,
getAudioWaveform, getAudioWaveform,
getRawMediaUploadData, getRawMediaUploadData,
getStream,
type MediaDownloadOptions type MediaDownloadOptions
} from './messages-media' } from './messages-media'
import { shouldIncludeReportingToken } from './reporting-utils' 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 * 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 * 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 (hasMedia && mediaOptions) {
if (card.image) { if (card.image) {
const { imageMessage } = await prepareWAMessageMedia({ image: card.image }, mediaOptions) 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) { if (imageMessage && !imageMessage.jpegThumbnail) {
mediaOptions.logger?.warn( mediaOptions.logger?.warn(
{ cardTitle: card.title }, { cardTitle: card.title },