Compare commits

...

14 Commits

Author SHA1 Message Date
Renato Alcara 7436beaab3 feat: add pastParticipants support in history sync
Surfaces past group participant data from WhatsApp history sync messages.
Previously pastParticipants was dropped during event buffering because
it was overwritten instead of accumulated across chunks.

- src/Types/Events.ts: add pastParticipants to BaileysEventMap['messaging-history.set'] and BufferedEventData.historySets
- src/Utils/event-buffer.ts: accumulate pastParticipants via spread array instead of overwriting; include in consolidateEvents output
- src/Utils/history.ts: pass item.pastParticipants in processHistoryMessage return

Refs: WhiskeySockets/Baileys#2426

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 12:25:51 -03:00
Renato Alcara 5926da9493 Merge branch 'master' of https://github.com/rsalcara/InfiniteAPI 2026-03-19 12:22:07 -03:00
Renato Alcara a9e926b907 fix: correct w:mex QueryIds and response fields for newsletter operations (#302)
Reverse-engineered from WA Web JS bundle and live CDP interception.
All QueryIds and XWAPaths were wrong; mute/unmute also had wrong variables structure.

- FOLLOW QueryId: 7871414976211147 → 24404358912487870
- UNFOLLOW QueryId: 7238632346214362 → 9767147403369991
- MUTE QueryId: 29766401636284406 → 31938993655691868
- UNMUTE QueryId: 9864994326891137 → 31938993655691868 (same mutation as MUTE)
- xwa2_newsletter_follow: 'xwa2_newsletter_follow' → 'xwa2_newsletter_join_v2'
- xwa2_newsletter_unfollow: 'xwa2_newsletter_unfollow' → 'xwa2_newsletter_leave_v2'
- xwa2_newsletter_mute_v2: 'xwa2_newsletter_mute_v2' → 'xwa2_newsletter_update_user_setting'
- xwa2_newsletter_unmute_v2: 'xwa2_newsletter_unmute_v2' → 'xwa2_newsletter_update_user_setting'
- newsletterMute/Unmute variables: flat {newsletter_id} → {input: {newsletter_id, type, value}}

Fixes #2346
2026-03-19 11:49:42 -03:00
Renato Alcara 2b2c3f9155 Merge branch 'master' of https://github.com/rsalcara/InfiniteAPI 2026-03-19 11:24:01 -03:00
Renato Alcara 71fe69fd21 chore: update WhatsApp Web version to v2.3000.1035504971 (#301)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-19 06:33:42 -03:00
github-actions[bot] eecf6df2a2 chore: update proto/version to v2.3000.1035484955 (#300)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-19 01:48:35 -03:00
Renato Alcara d04a3e1af8 fix: restore working carousel send/render path (#297)
fix: restore working carousel send/render path (#297)
2026-03-18 20:30:18 -03:00
Renato Alcara 21462c26d5 chore: update WhatsApp Web version to v2.3000.1035406580 (#299)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 06:42:44 -03:00
github-actions[bot] fc2122b11a chore: update proto/version to v2.3000.1035370989 (#298)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-18 01:49:58 -03:00
Renato Alcara 11eed3aedc chore: update WhatsApp Web version to v2.3000.1035317910 (#296)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 06:16:35 -03:00
github-actions[bot] dbc6dd6aed chore: update proto/version to v2.3000.1035302375 (#295)
Co-authored-by: rsalcara <rsalcara@users.noreply.github.com>
2026-03-17 00:46:29 -03:00
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 6060293793 fix: jimp detection for thumbnail generation
Fix typeof check: Jimp constructor is 'function' not 'object' in
jimp v1.6+. Without this fix, jpegThumbnail is never generated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:41:54 -03:00
Renato Alcara afce764ce9 Revert "fix: carousel renders on WhatsApp Web without F5"
This reverts commit f2c14f9ad6.
2026-03-07 00:41:38 -03:00
13 changed files with 1299 additions and 325 deletions
+14 -1
View File
@@ -1,7 +1,7 @@
syntax = "proto3";
package proto;
/// WhatsApp Version: 2.3000.1035216863
/// WhatsApp Version: 2.3000.1035484955
message ADVDeviceIdentity {
optional uint32 rawId = 1;
@@ -417,6 +417,7 @@ message BotCapabilityMetadata {
JSON_PATCH_STREAMING = 58;
AI_TAB_FORCE_CLIPPY = 59;
UNIFIED_RESPONSE_EMBEDDED_SCREENS = 60;
AI_SUBSCRIPTION_ENABLED = 61;
}
}
@@ -1933,8 +1934,20 @@ message HandshakeMessage {
optional bytes paddedBytes = 6;
optional bool sendServerHelloPaddedBytes = 7;
optional bool simulateXxkemFs = 8;
optional HandshakeMessage.HandshakePqMode pqMode = 9;
}
enum HandshakePqMode {
HANDSHAKE_PQ_MODE_UNKNOWN = 0;
XXKEM = 1;
XXKEM_FS = 2;
WA_CLASSICAL = 3;
WA_PQ = 4;
IKKEM = 5;
IKKEM_FS = 6;
XXKEM_2 = 7;
IKKEM_2 = 8;
}
message ServerHello {
optional bytes ephemeral = 1;
optional bytes static = 2;
+16 -1
View File
@@ -1092,7 +1092,8 @@ export namespace proto {
RICH_RESPONSE_UR_IMAGINE_VIDEO = 57,
JSON_PATCH_STREAMING = 58,
AI_TAB_FORCE_CLIPPY = 59,
UNIFIED_RESPONSE_EMBEDDED_SCREENS = 60
UNIFIED_RESPONSE_EMBEDDED_SCREENS = 60,
AI_SUBSCRIPTION_ENABLED = 61
}
}
@@ -4759,6 +4760,7 @@ export namespace proto {
paddedBytes?: (Uint8Array|null);
sendServerHelloPaddedBytes?: (boolean|null);
simulateXxkemFs?: (boolean|null);
pqMode?: (proto.HandshakeMessage.HandshakePqMode|null);
}
class ClientHello implements IClientHello {
@@ -4771,6 +4773,7 @@ export namespace proto {
public paddedBytes?: (Uint8Array|null);
public sendServerHelloPaddedBytes?: (boolean|null);
public simulateXxkemFs?: (boolean|null);
public pqMode?: (proto.HandshakeMessage.HandshakePqMode|null);
public static create(properties?: proto.HandshakeMessage.IClientHello): proto.HandshakeMessage.ClientHello;
public static encode(m: proto.HandshakeMessage.IClientHello, w?: $protobuf.Writer): $protobuf.Writer;
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.HandshakeMessage.ClientHello;
@@ -4780,6 +4783,18 @@ export namespace proto {
public static getTypeUrl(typeUrlPrefix?: string): string;
}
enum HandshakePqMode {
HANDSHAKE_PQ_MODE_UNKNOWN = 0,
XXKEM = 1,
XXKEM_FS = 2,
WA_CLASSICAL = 3,
WA_PQ = 4,
IKKEM = 5,
IKKEM_FS = 6,
XXKEM_2 = 7,
IKKEM_2 = 8
}
interface IServerHello {
ephemeral?: (Uint8Array|null);
"static"?: (Uint8Array|null);
+81
View File
@@ -6897,6 +6897,10 @@ export const proto = $root.proto = (() => {
case 60:
m.capabilities[i] = 60;
break;
case "AI_SUBSCRIPTION_ENABLED":
case 61:
m.capabilities[i] = 61;
break;
}
}
}
@@ -6993,6 +6997,7 @@ export const proto = $root.proto = (() => {
values[valuesById[58] = "JSON_PATCH_STREAMING"] = 58;
values[valuesById[59] = "AI_TAB_FORCE_CLIPPY"] = 59;
values[valuesById[60] = "UNIFIED_RESPONSE_EMBEDDED_SCREENS"] = 60;
values[valuesById[61] = "AI_SUBSCRIPTION_ENABLED"] = 61;
return values;
})();
@@ -32585,6 +32590,7 @@ export const proto = $root.proto = (() => {
ClientHello.prototype.paddedBytes = null;
ClientHello.prototype.sendServerHelloPaddedBytes = null;
ClientHello.prototype.simulateXxkemFs = null;
ClientHello.prototype.pqMode = null;
let $oneOfFields;
@@ -32636,6 +32642,12 @@ export const proto = $root.proto = (() => {
set: $util.oneOfSetter($oneOfFields)
});
// Virtual OneOf for proto3 optional field
Object.defineProperty(ClientHello.prototype, "_pqMode", {
get: $util.oneOfGetter($oneOfFields = ["pqMode"]),
set: $util.oneOfSetter($oneOfFields)
});
ClientHello.create = function create(properties) {
return new ClientHello(properties);
};
@@ -32659,6 +32671,8 @@ export const proto = $root.proto = (() => {
w.uint32(56).bool(m.sendServerHelloPaddedBytes);
if (m.simulateXxkemFs != null && Object.hasOwnProperty.call(m, "simulateXxkemFs"))
w.uint32(64).bool(m.simulateXxkemFs);
if (m.pqMode != null && Object.hasOwnProperty.call(m, "pqMode"))
w.uint32(72).int32(m.pqMode);
return w;
};
@@ -32703,6 +32717,10 @@ export const proto = $root.proto = (() => {
m.simulateXxkemFs = r.bool();
break;
}
case 9: {
m.pqMode = r.int32();
break;
}
default:
r.skipType(t & 7);
break;
@@ -32754,6 +32772,50 @@ export const proto = $root.proto = (() => {
if (d.simulateXxkemFs != null) {
m.simulateXxkemFs = Boolean(d.simulateXxkemFs);
}
switch (d.pqMode) {
default:
if (typeof d.pqMode === "number") {
m.pqMode = d.pqMode;
break;
}
break;
case "HANDSHAKE_PQ_MODE_UNKNOWN":
case 0:
m.pqMode = 0;
break;
case "XXKEM":
case 1:
m.pqMode = 1;
break;
case "XXKEM_FS":
case 2:
m.pqMode = 2;
break;
case "WA_CLASSICAL":
case 3:
m.pqMode = 3;
break;
case "WA_PQ":
case 4:
m.pqMode = 4;
break;
case "IKKEM":
case 5:
m.pqMode = 5;
break;
case "IKKEM_FS":
case 6:
m.pqMode = 6;
break;
case "XXKEM_2":
case 7:
m.pqMode = 7;
break;
case "IKKEM_2":
case 8:
m.pqMode = 8;
break;
}
return m;
};
@@ -32801,6 +32863,11 @@ export const proto = $root.proto = (() => {
if (o.oneofs)
d._simulateXxkemFs = "simulateXxkemFs";
}
if (m.pqMode != null && m.hasOwnProperty("pqMode")) {
d.pqMode = o.enums === String ? $root.proto.HandshakeMessage.HandshakePqMode[m.pqMode] === undefined ? m.pqMode : $root.proto.HandshakeMessage.HandshakePqMode[m.pqMode] : m.pqMode;
if (o.oneofs)
d._pqMode = "pqMode";
}
return d;
};
@@ -32818,6 +32885,20 @@ export const proto = $root.proto = (() => {
return ClientHello;
})();
HandshakeMessage.HandshakePqMode = (function() {
const valuesById = {}, values = Object.create(valuesById);
values[valuesById[0] = "HANDSHAKE_PQ_MODE_UNKNOWN"] = 0;
values[valuesById[1] = "XXKEM"] = 1;
values[valuesById[2] = "XXKEM_FS"] = 2;
values[valuesById[3] = "WA_CLASSICAL"] = 3;
values[valuesById[4] = "WA_PQ"] = 4;
values[valuesById[5] = "IKKEM"] = 5;
values[valuesById[6] = "IKKEM_FS"] = 6;
values[valuesById[7] = "XXKEM_2"] = 7;
values[valuesById[8] = "IKKEM_2"] = 8;
return values;
})();
HandshakeMessage.ServerHello = (function() {
function ServerHello(p) {
+1 -1
View File
@@ -1 +1 @@
{"version":[2,3000,1035223526]}
{"version":[2,3000,1035504971]}
+1095 -216
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -123,11 +123,11 @@ export const makeNewsletterSocket = (config: SocketConfig) => {
},
newsletterMute: (jid: string) => {
return executeWMexQuery({ newsletter_id: jid }, QueryIds.MUTE, XWAPaths.xwa2_newsletter_mute_v2)
return executeWMexQuery({ input: { newsletter_id: jid, type: 'MUTE_ADMIN_ACTIVITY', value: 'OFF' } }, QueryIds.MUTE, XWAPaths.xwa2_newsletter_mute_v2)
},
newsletterUnmute: (jid: string) => {
return executeWMexQuery({ newsletter_id: jid }, QueryIds.UNMUTE, XWAPaths.xwa2_newsletter_unmute_v2)
return executeWMexQuery({ input: { newsletter_id: jid, type: 'MUTE_ADMIN_ACTIVITY', value: 'ON' } }, QueryIds.UNMUTE, XWAPaths.xwa2_newsletter_unmute_v2)
},
newsletterUpdateName: async (jid: string, name: string) => {
+2
View File
@@ -27,6 +27,7 @@ export type BaileysEventMap = {
chats: Chat[]
contacts: Contact[]
messages: WAMessage[]
pastParticipants?: proto.IPastParticipants[] | null
isLatest?: boolean
progress?: number | null
syncType?: proto.HistorySync.HistorySyncType | null
@@ -185,6 +186,7 @@ export type BufferedEventData = {
chats: { [jid: string]: Chat }
contacts: { [jid: string]: Contact }
messages: { [uqId: string]: WAMessage }
pastParticipants?: proto.IPastParticipants[]
empty: boolean
isLatest: boolean
progress?: number | null
+8 -8
View File
@@ -4,10 +4,10 @@ export enum XWAPaths {
xwa2_newsletter_view = 'xwa2_newsletter_view',
xwa2_newsletter_metadata = 'xwa2_newsletter',
xwa2_newsletter_admin_count = 'xwa2_newsletter_admin',
xwa2_newsletter_mute_v2 = 'xwa2_newsletter_mute_v2',
xwa2_newsletter_unmute_v2 = 'xwa2_newsletter_unmute_v2',
xwa2_newsletter_follow = 'xwa2_newsletter_follow',
xwa2_newsletter_unfollow = 'xwa2_newsletter_unfollow',
xwa2_newsletter_mute_v2 = 'xwa2_newsletter_update_user_setting',
xwa2_newsletter_unmute_v2 = 'xwa2_newsletter_update_user_setting',
xwa2_newsletter_follow = 'xwa2_newsletter_join_v2',
xwa2_newsletter_unfollow = 'xwa2_newsletter_leave_v2',
xwa2_newsletter_change_owner = 'xwa2_newsletter_change_owner',
xwa2_newsletter_demote = 'xwa2_newsletter_demote',
xwa2_newsletter_delete_v2 = 'xwa2_newsletter_delete_v2'
@@ -17,10 +17,10 @@ export enum QueryIds {
UPDATE_METADATA = '24250201037901610',
METADATA = '6563316087068696',
SUBSCRIBERS = '9783111038412085',
FOLLOW = '7871414976211147',
UNFOLLOW = '7238632346214362',
MUTE = '29766401636284406',
UNMUTE = '9864994326891137',
FOLLOW = '24404358912487870',
UNFOLLOW = '9767147403369991',
MUTE = '31938993655691868',
UNMUTE = '31938993655691868',
ADMIN_COUNT = '7130823597031706',
CHANGE_OWNER = '7341777602580933',
DEMOTE = '6551828931592903',
+5
View File
@@ -967,6 +967,10 @@ function append<E extends BufferableEvent>(
}
}
data.historySets.pastParticipants = [
...(data.historySets.pastParticipants || []),
...(eventData.pastParticipants || [])
]
data.historySets.empty = false
data.historySets.syncType = eventData.syncType
data.historySets.progress = eventData.progress
@@ -1292,6 +1296,7 @@ function consolidateEvents(data: BufferedEventData) {
chats: Object.values(data.historySets.chats),
messages: Object.values(data.historySets.messages),
contacts: Object.values(data.historySets.contacts),
pastParticipants: data.historySets.pastParticipants,
syncType: data.historySets.syncType,
progress: data.historySets.progress,
isLatest: data.historySets.isLatest,
+1
View File
@@ -378,6 +378,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger
chats,
contacts,
messages,
pastParticipants: item.pastParticipants,
lidPnMappings,
syncType: item.syncType,
progress: item.progress
+1 -1
View File
@@ -160,7 +160,7 @@ export const extractImageThumb = async (bufferOrFilePath: Readable | Buffer | st
height: dimensions.height
}
}
} else if ('jimp' in lib && (typeof lib.jimp?.Jimp === 'object' || typeof lib.jimp?.Jimp === 'function')) {
} else if ('jimp' in lib && typeof lib.jimp?.Jimp === 'function') {
const jimp = await (lib.jimp.Jimp as any).read(bufferOrFilePath)
const dimensions = {
width: jimp.width,
+70 -94
View File
@@ -45,8 +45,8 @@ import {
generateThumbnail,
getAudioDuration,
getAudioWaveform,
getRawMediaUploadData,
getStream,
getRawMediaUploadData,
type MediaDownloadOptions
} from './messages-media'
import { shouldIncludeReportingToken } from './reporting-utils'
@@ -475,48 +475,6 @@ 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
@@ -677,6 +635,59 @@ export const generateCarouselMessage = async (
)
}
const recoverCarouselImageMetadata = async (
imageMessage: proto.Message.IImageMessage | null | undefined,
cardImage: WAMediaUpload,
cardTitle: string | undefined,
uploadOptions: MessageContentGenerationOptions
) => {
if (!imageMessage) {
return
}
const missingThumbnail = !imageMessage.jpegThumbnail
const missingWidth = !imageMessage.width
const missingHeight = !imageMessage.height
if (!missingThumbnail && !missingWidth && !missingHeight) {
return
}
try {
const { stream } = await getStream(cardImage, uploadOptions.options)
const { buffer, original } = await extractImageThumb(stream)
if (missingThumbnail) {
imageMessage.jpegThumbnail = buffer.toString('base64')
}
if (missingWidth && original.width) {
imageMessage.width = original.width
}
if (missingHeight && original.height) {
imageMessage.height = original.height
}
uploadOptions.logger?.info(
{
cardTitle,
recoveredThumbnail: !!imageMessage.jpegThumbnail,
width: imageMessage.width,
height: imageMessage.height
},
'[CAROUSEL] Recovered image metadata from source media'
)
} catch (error) {
uploadOptions.logger?.warn(
{
cardTitle,
trace: error instanceof Error ? error.stack : String(error)
},
'[CAROUSEL] Failed source-media thumbnail fallback'
)
}
}
// Map cards to the carousel format (processing media)
const carouselCards = await Promise.all(
cards.map(async card => {
@@ -692,10 +703,12 @@ export const generateCarouselMessage = async (
if (hasMedia && mediaOptions) {
if (card.image) {
const { imageMessage } = await prepareWAMessageMedia({ image: card.image }, mediaOptions)
if (imageMessage) {
await recoverCarouselImageMetadata(card, imageMessage, mediaOptions)
}
// Mirror the working Pastorini-style result: every carousel image card should
// carry a jpegThumbnail and dimensions before it reaches the Web live renderer.
await recoverCarouselImageMetadata(imageMessage, card.image, card.title, mediaOptions)
// Validate image fields needed for WhatsApp rendering
if (imageMessage && !imageMessage.jpegThumbnail) {
mediaOptions.logger?.warn(
{ cardTitle: card.title },
@@ -1275,57 +1288,20 @@ export const generateWAMessageContent = async (
options.logger?.info('Sending CTA buttons as nativeFlowMessage with viewOnceMessage wrapper')
}
}
// Check for nativeCarousel — inline handler (validated on Android, iOS, Web)
// Direct interactiveMessage at root (field 45), NO viewOnceMessage wrapper,
// NO messageContextInfo, NO biz/bot stanza nodes needed
// Check for nativeCarousel
else if (hasNonNullishProperty(message, 'nativeCarousel')) {
const carouselMsg = message as any
const cards = carouselMsg.nativeCarousel.cards || []
const title = carouselMsg.nativeCarousel.title || carouselMsg.title
const text = carouselMsg.text
const footer = carouselMsg.footer
const carouselCards = await Promise.all(
cards.map(async (card: any) => {
const hasMedia = !!(card.image || card.video)
const header: any = {
title: card.title || '',
subtitle: card.footer || '',
hasMediaAttachment: hasMedia
}
if (hasMedia && card.image) {
const { imageMessage } = await prepareWAMessageMedia({ image: card.image }, options)
if (imageMessage && !imageMessage.height) imageMessage.height = 500
if (imageMessage && !imageMessage.width) imageMessage.width = 500
header.imageMessage = imageMessage
}
return {
header,
body: { text: card.body || '' },
footer: card.footer ? { text: card.footer } : undefined,
nativeFlowMessage: {
buttons: (card.buttons || []).map((btn: any) => {
switch (btn.type) {
case 'url': return { name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: btn.text, url: btn.url, merchant_url: btn.url }) }
case 'copy': return { name: 'cta_copy', buttonParamsJson: JSON.stringify({ display_text: btn.text, copy_code: btn.copyText }) }
case 'call': return { name: 'cta_call', buttonParamsJson: JSON.stringify({ display_text: btn.text, phone_number: btn.phoneNumber }) }
default: return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: btn.text, id: btn.id }) }
}
})
}
}
})
)
m.interactiveMessage = {
header: { title: title || ' ', hasMediaAttachment: false },
body: { text: text || '' },
footer: footer ? { text: footer } : undefined,
carouselMessage: {
cards: carouselCards,
messageVersion: 1
}
const carouselOptions: CarouselMessageOptions = {
cards: carouselMsg.nativeCarousel.cards,
title: carouselMsg.nativeCarousel.title || carouselMsg.title,
text: carouselMsg.text,
footer: carouselMsg.footer
}
// Pass options for media processing if cards have images/videos
const generated = await generateCarouselMessage(carouselOptions, options)
// Frida capture shows interactiveMessage DIRECT (field 45) in DSM — no viewOnceMessage wrapper
// Testing without wrapper: biz node + quality_control already match Pastorini CDP stanza
m.interactiveMessage = generated.interactiveMessage
return m
}
// Check for nativeList
+3 -1
View File
@@ -158,11 +158,13 @@ export async function storeTcTokensFromIqResult({
...existingEntry,
token: Buffer.from(tokenNode.content),
timestamp: tokenNode.attrs.t,
// Resets real_issue_timestamp to null when storing a new token
// WABA Android: resets real_issue_timestamp to null when storing a new token
// (UPDATE wa_trusted_contacts_send SET real_issue_timestamp=null)
realIssueTimestamp: null
}
// Store under resolved storageJid AND under fallbackJid (PN) for reliable lookup
// The read path may resolve to a different LID than the store path
const normalizedFallback = jidNormalizedUser(fallbackJid)
const keysToStore: Record<string, typeof tokenEntry | null> = {
[storageJid]: tokenEntry