diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index e02045da..b14caa44 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -85,6 +85,8 @@ export const makeChatsSocket = (config: SocketConfig) => { sendUnifiedSession } = sock + const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping) + let privacySettings: { [_: string]: string } | undefined let syncState: SyncState = SyncState.Connecting @@ -752,7 +754,7 @@ export const makeChatsSocket = (config: SocketConfig) => { authState, jid: normalizedJid, baseContent, - getLIDForPN: signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping) + getLIDForPN }) } @@ -858,11 +860,12 @@ export const makeChatsSocket = (config: SocketConfig) => { * @param tcToken token for subscription, use if present */ const presenceSubscribe = async (toJid: string) => { - const tcTokenContent = await buildTcTokenFromJid({ - authState, - jid: toJid, - getLIDForPN: signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping) - }) + // Only include tctoken for user JIDs — groups/newsletters don't use tctokens + const normalizedToJid = jidNormalizedUser(toJid) + const isUserJid = isAnyPnUser(normalizedToJid) || isAnyLidUser(normalizedToJid) + const tcTokenContent = isUserJid + ? await buildTcTokenFromJid({ authState, jid: normalizedToJid, getLIDForPN }) + : undefined return sendNode({ tag: 'presence', diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 6266f489..a11e3cdc 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -123,6 +123,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { getPrivacyTokens } = sock + const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping) + /** this mutex ensures that each retryRequest will wait for the previous one to finish */ const retryMutex = makeMutex() @@ -720,10 +722,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { // When a session is refreshed (identity change), re-issue tctoken fire-and-forget if(result.action === 'session_refreshed') { const normalizedJid = jidNormalizedUser(from) - resolveTcTokenJid( - normalizedJid, - signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping) - ).then(async (tcJid) => { + resolveTcTokenJid(normalizedJid, getLIDForPN).then(async (tcJid) => { const tcData = await authState.keys.get('tctoken', [tcJid]) const entry = tcData[tcJid] if(entry?.token?.length && !isTcTokenExpired(entry.timestamp)) { @@ -1064,7 +1063,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { if(!tokensNode) return const tokenNodes = getBinaryNodeChildren(tokensNode, 'token') - const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping) for(const tokenNode of tokenNodes) { const { attrs, content } = tokenNode diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 7fd9d00d..ca47f193 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -100,6 +100,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { groupToggleEphemeral } = sock + const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping) + const userDevicesCache = config.userDevicesCache || new NodeCache({ @@ -1456,10 +1458,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { // Resolve destination to LID for tctoken storage — matches Signal session key pattern const tcTokenJid = is1on1Send - ? await resolveTcTokenJid( - destinationJid, - signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping) - ) + ? await resolveTcTokenJid(destinationJid, getLIDForPN) : destinationJid const contactTcTokenData = is1on1Send ? await authState.keys.get('tctoken', [tcTokenJid]) : {} const existingTokenEntry = contactTcTokenData[tcTokenJid] @@ -1488,7 +1487,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { result: fetchResult, fallbackJid: destinationJid, keys: authState.keys, - getLIDForPN: signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping) + getLIDForPN // onNewJidStored not passed — the pruning index lives in messages-recv (higher layer) }) }) @@ -1594,38 +1593,37 @@ export const makeMessagesSocket = (config: SocketConfig) => { await sendNode(stanza) - // Fire-and-forget: issue our token to the contact (like WA Web's sendTcToken) - // Only for 1:1 sends where we already have a token, and when bucket boundary crossed - if (is1on1Send && tcTokenBuffer?.length && shouldSendNewTcToken(existingTokenEntry?.senderTimestamp)) { + // Fire-and-forget: issue our token to the contact (like WA Web's sendTcToken). + // Gated only by shouldSendNewTcToken — removed tcTokenBuffer?.length guard so + // issuance fires even when we don't yet hold a token (bucket boundary crossed). + // IMPORTANT: must run AFTER sendNode — issuing before the message causes error 463. + if (is1on1Send && shouldSendNewTcToken(existingTokenEntry?.senderTimestamp)) { const issueTimestamp = unixTimestampSeconds() logTcToken('reissue', { jid: destinationJid }) - // WA Web writes senderTimestamp only AFTER the IQ succeeds - // (WAWebSendTcTokenChatAction.sendTcToken). - // This ensures failed issuance allows re-issuance on the next message - // rather than blocking it for up to 7 days (one bucket duration). getPrivacyTokens([destinationJid], issueTimestamp) .then(async result => { - // Store any tokens received in the IQ response + // Store any tokens received in the IQ response. + // onNewJidStored not passed — pruning index lives in messages-recv (higher layer). await storeTcTokensFromIqResult({ result, fallbackJid: tcTokenJid, keys: authState.keys, - getLIDForPN: signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping) + getLIDForPN }) - // Re-read entry to avoid overwriting concurrent notification handler updates + // Persist senderTimestamp unconditionally — WA Web stores it in the chat table + // regardless of whether a token exists. Spread preserves token+timestamp if present. const currentData = await authState.keys.get('tctoken', [tcTokenJid]) const currentEntry = currentData[tcTokenJid] - if (currentEntry?.token?.length) { - await authState.keys.set({ - tctoken: { - [tcTokenJid]: { - ...currentEntry, - senderTimestamp: issueTimestamp - } + await authState.keys.set({ + tctoken: { + [tcTokenJid]: { + token: Buffer.alloc(0), + ...currentEntry, + senderTimestamp: issueTimestamp } - }) - } + } + }) logTcToken('reissue_ok', { jid: destinationJid }) })