@itsliaaa/baileys 0.1.32 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +328 -74
- package/WAProto/index.js +22 -18
- package/lib/Defaults/index.js +2 -0
- package/lib/Socket/chats.js +223 -57
- package/lib/Socket/groups.js +6 -0
- package/lib/Socket/messages-recv.js +230 -53
- package/lib/Socket/messages-send.js +78 -7
- package/lib/Utils/chat-utils.js +34 -7
- package/lib/Utils/decode-wa-message.js +14 -0
- package/lib/Utils/event-buffer.js +2 -0
- package/lib/Utils/generics.js +9 -0
- package/lib/Utils/history.js +11 -9
- package/lib/Utils/identity-change-handler.js +1 -0
- package/lib/Utils/messages-media.js +1 -1
- package/lib/Utils/messages.js +21 -6
- package/lib/Utils/process-message.js +53 -1
- package/lib/Utils/rich-message-utils.js +40 -35
- package/lib/Utils/sync-action-utils.js +1 -0
- package/lib/Utils/tc-token-utils.js +151 -5
- package/lib/Utils/use-single-file-auth-state.js +19 -26
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +26 -3
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +22 -0
- package/lib/WAUSync/Protocols/index.js +2 -1
- package/lib/WAUSync/USyncQuery.js +5 -1
- package/lib/WAUSync/USyncUser.js +8 -0
- package/package.json +2 -2
|
@@ -33,6 +33,7 @@ export const DECRYPTION_RETRY_CONFIG = {
|
|
|
33
33
|
baseDelayMs: 100,
|
|
34
34
|
sessionRecordErrors: ['No session record', 'SessionError: No session record']
|
|
35
35
|
};
|
|
36
|
+
/** NACK reason codes we send to the server (client → server) */
|
|
36
37
|
export const NACK_REASONS = {
|
|
37
38
|
ParsingError: 487,
|
|
38
39
|
UnrecognizedStanza: 488,
|
|
@@ -48,6 +49,17 @@ export const NACK_REASONS = {
|
|
|
48
49
|
UnsupportedLIDGroup: 551,
|
|
49
50
|
DBOperationFailed: 552
|
|
50
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Server-side error codes returned in ack stanzas (server → client) that we
|
|
54
|
+
* currently have dedicated handlers for. Extend as more handlers are added.
|
|
55
|
+
* Distinct from the client-side NackReason enum (WAWebCreateNackFromStanza).
|
|
56
|
+
*/
|
|
57
|
+
export const SERVER_ERROR_CODES = {
|
|
58
|
+
/** 1:1 message missing privacy token (tctoken) */
|
|
59
|
+
MissingTcToken: '463',
|
|
60
|
+
/** Stanza validation failure (SMAX_INVALID) — likely stale device session */
|
|
61
|
+
SmaxInvalid: '479'
|
|
62
|
+
};
|
|
51
63
|
export const extractAddressingContext = (stanza) => {
|
|
52
64
|
let senderAlt;
|
|
53
65
|
let recipientAlt;
|
|
@@ -148,10 +160,12 @@ export function decodeMessageNode(stanza, meId, meLid) {
|
|
|
148
160
|
const key = {
|
|
149
161
|
remoteJid: chatId,
|
|
150
162
|
remoteJidAlt: !isJidGroup(chatId) ? addressingContext.senderAlt : undefined,
|
|
163
|
+
remoteJidUsername: !isJidGroup(chatId) ? stanza.attrs.peer_recipient_username || stanza.attrs.recipient_username : undefined,
|
|
151
164
|
fromMe,
|
|
152
165
|
id: msgId,
|
|
153
166
|
participant,
|
|
154
167
|
participantAlt: isJidGroup(chatId) ? addressingContext.senderAlt : undefined,
|
|
168
|
+
participantUsername: stanza.attrs.participant ? stanza.attrs.participant_username : undefined,
|
|
155
169
|
addressingMode: addressingContext.addressingMode,
|
|
156
170
|
...(msgType === 'newsletter' && stanza.attrs.server_id ? { server_id: stanza.attrs.server_id } : {})
|
|
157
171
|
};
|
|
@@ -252,6 +252,7 @@ eventData, logger) {
|
|
|
252
252
|
data.historySets.empty = false;
|
|
253
253
|
data.historySets.syncType = eventData.syncType;
|
|
254
254
|
data.historySets.progress = eventData.progress;
|
|
255
|
+
data.historySets.chunkOrder = eventData.chunkOrder;
|
|
255
256
|
data.historySets.peerDataRequestSessionId = eventData.peerDataRequestSessionId;
|
|
256
257
|
data.historySets.isLatest = eventData.isLatest || data.historySets.isLatest;
|
|
257
258
|
break;
|
|
@@ -519,6 +520,7 @@ function consolidateEvents(data) {
|
|
|
519
520
|
syncType: data.historySets.syncType,
|
|
520
521
|
progress: data.historySets.progress,
|
|
521
522
|
isLatest: data.historySets.isLatest,
|
|
523
|
+
chunkOrder: data.historySets.chunkOrder,
|
|
522
524
|
peerDataRequestSessionId: data.historySets.peerDataRequestSessionId
|
|
523
525
|
};
|
|
524
526
|
}
|
package/lib/Utils/generics.js
CHANGED
|
@@ -316,6 +316,15 @@ export const getCallStatusFromNode = ({ tag, attrs }) => {
|
|
|
316
316
|
status = 'terminate';
|
|
317
317
|
}
|
|
318
318
|
break;
|
|
319
|
+
case 'preaccept':
|
|
320
|
+
status = 'preaccept';
|
|
321
|
+
break;
|
|
322
|
+
case 'transport':
|
|
323
|
+
status = 'transport';
|
|
324
|
+
break;
|
|
325
|
+
case 'relaylatency':
|
|
326
|
+
status = 'relaylatency';
|
|
327
|
+
break;
|
|
319
328
|
case 'reject':
|
|
320
329
|
status = 'reject';
|
|
321
330
|
break;
|
package/lib/Utils/history.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { pipeline } from 'stream/promises';
|
|
1
2
|
import { promisify } from 'util';
|
|
2
|
-
import { inflate } from 'zlib';
|
|
3
|
+
import { createInflate, inflate } from 'zlib';
|
|
3
4
|
import { proto } from '../../WAProto/index.js';
|
|
4
5
|
import { WAMessageStubType } from '../Types/index.js';
|
|
5
6
|
import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser } from '../WABinary/index.js';
|
|
@@ -24,13 +25,13 @@ const extractPnFromMessages = (messages) => {
|
|
|
24
25
|
};
|
|
25
26
|
export const downloadHistory = async (msg, options) => {
|
|
26
27
|
const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options });
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
buffer =
|
|
28
|
+
// Pipe decrypted stream directly through zlib inflate
|
|
29
|
+
// This avoids allocating an intermediate buffer for the compressed data
|
|
30
|
+
const inflater = createInflate();
|
|
31
|
+
const chunks = [];
|
|
32
|
+
inflater.on('data', (chunk) => chunks.push(chunk));
|
|
33
|
+
await pipeline(stream, inflater);
|
|
34
|
+
const buffer = Buffer.concat(chunks);
|
|
34
35
|
const syncData = proto.HistorySync.decode(buffer);
|
|
35
36
|
return syncData;
|
|
36
37
|
};
|
|
@@ -55,6 +56,7 @@ export const processHistoryMessage = (item, logger) => {
|
|
|
55
56
|
contacts.push({
|
|
56
57
|
id: chat.id,
|
|
57
58
|
name: chat.displayName || chat.name || chat.username || undefined,
|
|
59
|
+
username: chat.username || undefined,
|
|
58
60
|
lid: chat.lidJid || chat.accountLid || undefined,
|
|
59
61
|
phoneNumber: chat.pnJid || undefined
|
|
60
62
|
});
|
|
@@ -95,7 +97,7 @@ export const processHistoryMessage = (item, logger) => {
|
|
|
95
97
|
});
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
|
-
chats.push(
|
|
100
|
+
chats.push(chat);
|
|
99
101
|
}
|
|
100
102
|
break;
|
|
101
103
|
case proto.HistorySync.HistorySyncType.PUSH_NAME:
|
|
@@ -37,6 +37,7 @@ export async function handleIdentityChange(node, ctx) {
|
|
|
37
37
|
ctx.logger.debug({ jid: from }, 'skipping session refresh during offline processing');
|
|
38
38
|
return { action: 'skipped_offline' };
|
|
39
39
|
}
|
|
40
|
+
ctx.onBeforeSessionRefresh?.(from);
|
|
40
41
|
try {
|
|
41
42
|
await ctx.assertSessions([from], true);
|
|
42
43
|
return { action: 'session_refreshed' };
|
|
@@ -516,7 +516,7 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
|
|
|
516
516
|
};
|
|
517
517
|
const output = new Transform({
|
|
518
518
|
transform(chunk, _, callback) {
|
|
519
|
-
let data = Buffer.concat([remainingBytes, chunk]);
|
|
519
|
+
let data = remainingBytes.length ? Buffer.concat([remainingBytes, chunk]) : chunk;
|
|
520
520
|
const decryptLength = toSmallestChunkSize(data.length);
|
|
521
521
|
remainingBytes = data.slice(decryptLength);
|
|
522
522
|
data = data.slice(0, decryptLength);
|
package/lib/Utils/messages.js
CHANGED
|
@@ -657,6 +657,7 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
657
657
|
else if (hasNonNullishProperty(message, 'code') ||
|
|
658
658
|
hasNonNullishProperty(message, 'expressions') ||
|
|
659
659
|
hasNonNullishProperty(message, 'items') ||
|
|
660
|
+
hasNonNullishProperty(message, 'links') ||
|
|
660
661
|
hasNonNullishProperty(message, 'table') ||
|
|
661
662
|
hasNonNullishProperty(message, 'richResponse')) {
|
|
662
663
|
m = prepareRichResponseMessage(message);
|
|
@@ -673,6 +674,7 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
673
674
|
extContent.description = urlInfo.description;
|
|
674
675
|
extContent.title = urlInfo.title;
|
|
675
676
|
extContent.previewType = urlInfo.previewType ?? 0;
|
|
677
|
+
extContent.linkPreviewMetadata = urlInfo.linkPreviewMetadata;
|
|
676
678
|
const img = urlInfo.highQualityThumbnail;
|
|
677
679
|
if (img) {
|
|
678
680
|
extContent.thumbnailDirectPath = img.directPath;
|
|
@@ -684,6 +686,19 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
684
686
|
extContent.thumbnailEncSha256 = img.fileEncSha256;
|
|
685
687
|
}
|
|
686
688
|
}
|
|
689
|
+
let faviconInfo = message.favicon;
|
|
690
|
+
if (faviconInfo) {
|
|
691
|
+
faviconInfo = await generateWAMessageMedia(faviconInfo, options);
|
|
692
|
+
extContent.faviconMMSMetadata = {
|
|
693
|
+
thumbnailDirectPath: faviconInfo.directPath,
|
|
694
|
+
mediaKey: faviconInfo.mediaKey,
|
|
695
|
+
mediaKeyTimestamp: faviconInfo.mediaKeyTimestamp,
|
|
696
|
+
thumbnailWidth: 32,
|
|
697
|
+
thumbnailHeight: 32,
|
|
698
|
+
thumbnailSha256: faviconInfo.fileSha256,
|
|
699
|
+
thumbnailEncSha256: faviconInfo.fileEncSha256
|
|
700
|
+
};
|
|
701
|
+
}
|
|
687
702
|
if (options.backgroundColor) {
|
|
688
703
|
extContent.backgroundArgb = await assertColor(options.backgroundColor);
|
|
689
704
|
}
|
|
@@ -1270,12 +1285,6 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
1270
1285
|
}
|
|
1271
1286
|
m = { invoiceMessage };
|
|
1272
1287
|
}
|
|
1273
|
-
if (shouldIncludeReportingToken(m)) {
|
|
1274
|
-
m.messageContextInfo = m.messageContextInfo || {};
|
|
1275
|
-
if (!m.messageContextInfo.messageSecret) {
|
|
1276
|
-
m.messageContextInfo.messageSecret = randomBytes(32);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
1288
|
// Lia@Changes 31-01-26 --- Add direct externalAdReply access (no need to create contextInfo first)
|
|
1280
1289
|
if (hasOptionalProperty(message, 'externalAdReply') && !!message.externalAdReply) {
|
|
1281
1290
|
const messageType = Object.keys(m)[0];
|
|
@@ -1391,6 +1400,12 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
1391
1400
|
}
|
|
1392
1401
|
}
|
|
1393
1402
|
}
|
|
1403
|
+
if (shouldIncludeReportingToken(m)) {
|
|
1404
|
+
m.messageContextInfo = m.messageContextInfo || {};
|
|
1405
|
+
if (!m.messageContextInfo.messageSecret) {
|
|
1406
|
+
m.messageContextInfo.messageSecret = randomBytes(32);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1394
1409
|
return proto.Message.create(m);
|
|
1395
1410
|
};
|
|
1396
1411
|
export const generateWAMessageFromContent = (jid, message, options) => {
|
|
@@ -5,6 +5,7 @@ import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJid
|
|
|
5
5
|
import { aesDecryptGCM, hmacSign } from './crypto.js';
|
|
6
6
|
import { getKeyAuthor, toNumber } from './generics.js';
|
|
7
7
|
import { downloadAndProcessHistorySyncNotification } from './history.js';
|
|
8
|
+
import { buildMergedTcTokenIndexWrite, resolveTcTokenJid } from './tc-token-utils.js';
|
|
8
9
|
const REAL_MSG_STUB_TYPES = new Set([
|
|
9
10
|
WAMessageStubType.CALL_MISSED_GROUP_VIDEO,
|
|
10
11
|
WAMessageStubType.CALL_MISSED_GROUP_VOICE,
|
|
@@ -12,6 +13,53 @@ const REAL_MSG_STUB_TYPES = new Set([
|
|
|
12
13
|
WAMessageStubType.CALL_MISSED_VOICE
|
|
13
14
|
]);
|
|
14
15
|
const REAL_MSG_REQ_ME_STUB_TYPES = new Set([WAMessageStubType.GROUP_PARTICIPANT_ADD]);
|
|
16
|
+
async function storeTcTokensFromHistorySync(chats, signalRepository, keyStore, logger) {
|
|
17
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
|
|
18
|
+
const candidates = [];
|
|
19
|
+
for (const chat of chats) {
|
|
20
|
+
const ts = chat.tcTokenTimestamp ? toNumber(chat.tcTokenTimestamp) : 0;
|
|
21
|
+
if (chat.tcToken?.length && ts > 0) {
|
|
22
|
+
const jid = jidNormalizedUser(chat.id);
|
|
23
|
+
const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
|
|
24
|
+
candidates.push({
|
|
25
|
+
storageJid,
|
|
26
|
+
token: Buffer.from(chat.tcToken),
|
|
27
|
+
ts,
|
|
28
|
+
senderTs: chat.tcTokenSenderTimestamp ? toNumber(chat.tcTokenSenderTimestamp) : undefined
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!candidates.length) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const jids = candidates.map(c => c.storageJid);
|
|
36
|
+
const existing = await keyStore.get('tctoken', jids);
|
|
37
|
+
const entries = {};
|
|
38
|
+
for (const c of candidates) {
|
|
39
|
+
const existingEntry = existing[c.storageJid];
|
|
40
|
+
const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0;
|
|
41
|
+
if (existingTs > 0 && existingTs >= c.ts) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
entries[c.storageJid] = {
|
|
45
|
+
...existingEntry,
|
|
46
|
+
token: c.token,
|
|
47
|
+
timestamp: String(c.ts),
|
|
48
|
+
...(c.senderTs !== undefined ? { senderTimestamp: c.senderTs } : {})
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (Object.keys(entries).length) {
|
|
52
|
+
logger?.debug({ count: Object.keys(entries).length }, 'storing tctokens from history sync');
|
|
53
|
+
try {
|
|
54
|
+
// Include updated __index so cross-session pruning picks these JIDs up.
|
|
55
|
+
const indexWrite = await buildMergedTcTokenIndexWrite(keyStore, Object.keys(entries));
|
|
56
|
+
await keyStore.set({ tctoken: { ...entries, ...indexWrite } });
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
logger?.warn({ err }, 'failed to store tctokens from history sync');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
15
63
|
/** Cleans a received message to further processing */
|
|
16
64
|
export const cleanMessage = (message, meId, meLid) => {
|
|
17
65
|
// ensure remoteJid and participant doesn't have device or agent in it
|
|
@@ -174,9 +222,11 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
174
222
|
.storeLIDPNMappings(data.lidPnMappings)
|
|
175
223
|
.catch(err => logger?.warn({ err }, 'failed to store LID-PN mappings from history sync'));
|
|
176
224
|
}
|
|
225
|
+
await storeTcTokensFromHistorySync(data.chats, signalRepository, keyStore, logger);
|
|
177
226
|
ev.emit('messaging-history.set', {
|
|
178
227
|
...data,
|
|
179
228
|
isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined,
|
|
229
|
+
chunkOrder: histNotification.chunkOrder,
|
|
180
230
|
peerDataRequestSessionId: histNotification.peerDataRequestSessionId
|
|
181
231
|
});
|
|
182
232
|
}
|
|
@@ -383,12 +433,13 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
383
433
|
id: jid,
|
|
384
434
|
author: message.key.participant,
|
|
385
435
|
authorPn: message.key.participantAlt,
|
|
436
|
+
authorUsername: message.key.participantUsername,
|
|
386
437
|
participants,
|
|
387
438
|
action
|
|
388
439
|
});
|
|
389
440
|
const emitGroupUpdate = (update) => {
|
|
390
441
|
ev.emit('groups.update', [
|
|
391
|
-
{ id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt }
|
|
442
|
+
{ id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername }
|
|
392
443
|
]);
|
|
393
444
|
};
|
|
394
445
|
const emitGroupRequestJoin = (participant, action, method) => {
|
|
@@ -396,6 +447,7 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
396
447
|
id: jid,
|
|
397
448
|
author: message.key.participant,
|
|
398
449
|
authorPn: message.key.participantAlt,
|
|
450
|
+
authorUsername: message.key.participantUsername,
|
|
399
451
|
participant: participant.lid,
|
|
400
452
|
participantPn: participant.pn,
|
|
401
453
|
action,
|
|
@@ -11,7 +11,6 @@ import { LANGUAGE_KEYWORDS } from '../WABinary/constants.js';
|
|
|
11
11
|
import { CodeHighlightType, RichSubMessageType } from '../Types/RichType.js';
|
|
12
12
|
import { proto } from '../../WAProto/index.js';
|
|
13
13
|
import { unixTimestampSeconds } from './generics.js';
|
|
14
|
-
const textEncoder = new TextEncoder();
|
|
15
14
|
const NOOP = new Set([]);
|
|
16
15
|
export const tokenizeCode = (code, language = 'javascript') => {
|
|
17
16
|
const keywords = LANGUAGE_KEYWORDS[language] || NOOP;
|
|
@@ -121,33 +120,11 @@ export const toUnified = (submessages) =>
|
|
|
121
120
|
}
|
|
122
121
|
};
|
|
123
122
|
case RichSubMessageType.TEXT:
|
|
124
|
-
const shouldAddInlineEntity = index == 0;
|
|
125
|
-
const inlineEntity = [{
|
|
126
|
-
key: 'Starseed',
|
|
127
|
-
metadata: {
|
|
128
|
-
reference_id: 1,
|
|
129
|
-
reference_url: DONATE_URL,
|
|
130
|
-
reference_title: 'For Donation via Saweria',
|
|
131
|
-
reference_display_name: 'Donate',
|
|
132
|
-
sources: [{
|
|
133
|
-
source_type: 'THIRD_PARTY',
|
|
134
|
-
source_display_name: 'Donate',
|
|
135
|
-
source_subtitle: '',
|
|
136
|
-
source_url: DONATE_URL
|
|
137
|
-
}],
|
|
138
|
-
__typename: 'GenAISearchCitationItem'
|
|
139
|
-
}
|
|
140
|
-
}];
|
|
141
|
-
const textEntity = shouldAddInlineEntity ?
|
|
142
|
-
'{{Starseed}}¹{{/Starseed}}' :
|
|
143
|
-
'';
|
|
144
123
|
return {
|
|
145
124
|
view_model: {
|
|
146
125
|
primitive: {
|
|
147
|
-
text: submessage.messageText
|
|
148
|
-
inline_entities:
|
|
149
|
-
inlineEntity :
|
|
150
|
-
[],
|
|
126
|
+
text: submessage.messageText,
|
|
127
|
+
inline_entities: submessage.inlineEntities || [],
|
|
151
128
|
__typename: 'GenAIMarkdownTextUXPrimitive'
|
|
152
129
|
},
|
|
153
130
|
__typename: 'GenAISingleLayoutViewModel'
|
|
@@ -212,14 +189,15 @@ export const buildAdditionalBotMetadataContext = (submessages) => {
|
|
|
212
189
|
return { sources, mediaDetailsMetadataList };
|
|
213
190
|
}
|
|
214
191
|
export const prepareRichResponseMessage = (content) => {
|
|
215
|
-
const { code, contentText, expressions, footerText, headerText, items, language, richResponse, table, text, title } = content;
|
|
192
|
+
const { code, contentText, expressions, footerText, headerText, items, language, links, noHeading, richResponse, table, text, title } = content;
|
|
216
193
|
let submessages = [];
|
|
217
194
|
if (Array.isArray(richResponse)) {
|
|
218
195
|
submessages = richResponse.map((submessage) => {
|
|
219
196
|
if (submessage.text) {
|
|
220
197
|
return {
|
|
221
198
|
messageType: RichSubMessageType.TEXT,
|
|
222
|
-
messageText: submessage.text
|
|
199
|
+
messageText: submessage.text,
|
|
200
|
+
inlineEntities: submessage.inlineEntities
|
|
223
201
|
};
|
|
224
202
|
}
|
|
225
203
|
else if (submessage.code) {
|
|
@@ -301,16 +279,42 @@ export const prepareRichResponseMessage = (content) => {
|
|
|
301
279
|
}
|
|
302
280
|
});
|
|
303
281
|
}
|
|
282
|
+
else if (links) {
|
|
283
|
+
links.forEach((linkField, index) => {
|
|
284
|
+
const prefix = 'SS_' + index;
|
|
285
|
+
const url = linkField.url || DONATE_URL;
|
|
286
|
+
const sources = linkField.sources?.map((sourceField) => ({
|
|
287
|
+
source_type: 'THIRD_PARTY',
|
|
288
|
+
source_display_name: sourceField.displayName || 'Donate',
|
|
289
|
+
source_subtitle: sourceField.subtitle || 'Saweria',
|
|
290
|
+
source_url: sourceField.url || url
|
|
291
|
+
}));
|
|
292
|
+
submessages.push({
|
|
293
|
+
messageType: RichSubMessageType.TEXT,
|
|
294
|
+
messageText: linkField.text + ` {{${prefix}}}¹{{/${prefix}}} `,
|
|
295
|
+
inlineEntities: [{
|
|
296
|
+
key: prefix,
|
|
297
|
+
metadata: {
|
|
298
|
+
reference_id: index + 1,
|
|
299
|
+
reference_url: url,
|
|
300
|
+
reference_title: linkField.title || 'For Donation via Saweria',
|
|
301
|
+
reference_display_name: linkField.displayName || 'Donation',
|
|
302
|
+
sources: sources || [],
|
|
303
|
+
__typename: 'GenAISearchCitationItem'
|
|
304
|
+
}
|
|
305
|
+
}]
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
304
309
|
else if (table) {
|
|
305
|
-
const tableRows = table.map((items, index) => ({
|
|
306
|
-
isHeading: index == 0,
|
|
307
|
-
items
|
|
308
|
-
}));
|
|
309
310
|
submessages.push({
|
|
310
311
|
messageType: RichSubMessageType.TABLE,
|
|
311
312
|
tableMetadata: {
|
|
312
313
|
title,
|
|
313
|
-
rows:
|
|
314
|
+
rows: table.map((items, index) => ({
|
|
315
|
+
isHeading: !noHeading && index == 0,
|
|
316
|
+
items
|
|
317
|
+
}))
|
|
314
318
|
}
|
|
315
319
|
});
|
|
316
320
|
}
|
|
@@ -323,16 +327,17 @@ export const prepareRichResponseMessage = (content) => {
|
|
|
323
327
|
}
|
|
324
328
|
const unified = toUnified(submessages);
|
|
325
329
|
const message = wrapToBotForwardedMessage({
|
|
326
|
-
submessages,
|
|
330
|
+
submessages: [],
|
|
327
331
|
messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
|
|
328
332
|
unifiedResponse: {
|
|
329
|
-
data:
|
|
333
|
+
data: Buffer.from(JSON.stringify(unified), 'utf-8') // Lia@Note 25-04-26 --- Expects "ArrayBufferLike"
|
|
330
334
|
},
|
|
331
335
|
contextInfo: {
|
|
332
336
|
isForwarded: true,
|
|
333
337
|
forwardingScore: 1,
|
|
334
338
|
forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' },
|
|
335
|
-
forwardOrigin: 4
|
|
339
|
+
forwardOrigin: 4,
|
|
340
|
+
botMessageSharingInfo: { forwardScore: 1 }
|
|
336
341
|
}
|
|
337
342
|
});
|
|
338
343
|
// Lia@Note 17-04-26 --- TODO: Fill mediaDetailsMetadataList and sources field
|
|
@@ -1,9 +1,119 @@
|
|
|
1
|
-
|
|
1
|
+
import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidMetaAI, isLidUser, isPnUser, jidNormalizedUser } from '../WABinary/index.js';
|
|
2
|
+
// Same phone-number pattern as WABinary's isJidBot, applied against the user
|
|
3
|
+
// part so the check is invariant to @c.us ↔ @s.whatsapp.net normalization.
|
|
4
|
+
const BOT_PHONE_REGEX = /^1313555\d{4}$|^131655500\d{2}$/;
|
|
5
|
+
/**
|
|
6
|
+
* Mirrors WA Web's `Wid.isRegularUser()` (user ∧ ¬PSA ∧ ¬Bot). Used to gate tctoken
|
|
7
|
+
* storage against malformed notifications — WA Web filters server-side but we
|
|
8
|
+
* defend here for parity with `WAWebSetTcTokenChatAction.handleIncomingTcToken`.
|
|
9
|
+
* Works for both pre- and post-normalized JIDs (`@c.us` vs `@s.whatsapp.net`).
|
|
10
|
+
*/
|
|
11
|
+
function isRegularUser(jid) {
|
|
12
|
+
if (!jid)
|
|
13
|
+
return false;
|
|
14
|
+
const user = jid.split('@')[0] ?? '';
|
|
15
|
+
if (user === '0')
|
|
16
|
+
return false; // PSA
|
|
17
|
+
if (BOT_PHONE_REGEX.test(user))
|
|
18
|
+
return false; // Bot by phone pattern
|
|
19
|
+
if (isJidMetaAI(jid))
|
|
20
|
+
return false; // MetaAI (@bot server)
|
|
21
|
+
return !!(isPnUser(jid) || isLidUser(jid) || isHostedPnUser(jid) || isHostedLidUser(jid) || jid.endsWith('@c.us'));
|
|
22
|
+
}
|
|
23
|
+
const TC_TOKEN_BUCKET_DURATION = 604800; // 7 days
|
|
24
|
+
const TC_TOKEN_NUM_BUCKETS = 4; // ~28-day rolling window
|
|
25
|
+
/** Sentinel key under `tctoken` store holding a JSON array of tracked storage JIDs for cross-session pruning. */
|
|
26
|
+
export const TC_TOKEN_INDEX_KEY = '__index';
|
|
27
|
+
/** Read the persisted tctoken JID index and return its entries (never contains the sentinel key itself). */
|
|
28
|
+
export async function readTcTokenIndex(keys) {
|
|
29
|
+
const data = await keys.get('tctoken', [TC_TOKEN_INDEX_KEY]);
|
|
30
|
+
const entry = data[TC_TOKEN_INDEX_KEY];
|
|
31
|
+
if (!entry?.token?.length)
|
|
32
|
+
return [];
|
|
2
33
|
try {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
34
|
+
const parsed = JSON.parse(Buffer.from(entry.token).toString());
|
|
35
|
+
if (!Array.isArray(parsed))
|
|
36
|
+
return [];
|
|
37
|
+
return parsed.filter((j) => typeof j === 'string' && j.length > 0 && j !== TC_TOKEN_INDEX_KEY);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Build a SignalDataSet fragment that writes the merged index (persisted ∪ added) under the sentinel key. */
|
|
44
|
+
export async function buildMergedTcTokenIndexWrite(keys, addedJids) {
|
|
45
|
+
const persisted = await readTcTokenIndex(keys);
|
|
46
|
+
const merged = new Set(persisted);
|
|
47
|
+
for (const jid of addedJids) {
|
|
48
|
+
if (jid && jid !== TC_TOKEN_INDEX_KEY)
|
|
49
|
+
merged.add(jid);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
[TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...merged])) }
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// WA Web has separate sender/receiver AB props for these but they're identical today
|
|
56
|
+
export function isTcTokenExpired(timestamp) {
|
|
57
|
+
if (timestamp === null || timestamp === undefined)
|
|
58
|
+
return true;
|
|
59
|
+
const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
|
|
60
|
+
if (isNaN(ts))
|
|
61
|
+
return true;
|
|
62
|
+
const now = Math.floor(Date.now() / 1000);
|
|
63
|
+
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
|
|
64
|
+
const cutoffBucket = currentBucket - (TC_TOKEN_NUM_BUCKETS - 1);
|
|
65
|
+
const cutoffTimestamp = cutoffBucket * TC_TOKEN_BUCKET_DURATION;
|
|
66
|
+
return ts < cutoffTimestamp;
|
|
67
|
+
}
|
|
68
|
+
export function shouldSendNewTcToken(senderTimestamp) {
|
|
69
|
+
if (senderTimestamp === undefined)
|
|
70
|
+
return true;
|
|
71
|
+
const now = Math.floor(Date.now() / 1000);
|
|
72
|
+
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
|
|
73
|
+
const senderBucket = Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION);
|
|
74
|
+
return currentBucket > senderBucket;
|
|
75
|
+
}
|
|
76
|
+
/** Resolve JID to LID for tctoken storage (WA Web stores under LID) */
|
|
77
|
+
export async function resolveTcTokenJid(jid, getLIDForPN) {
|
|
78
|
+
if (isLidUser(jid))
|
|
79
|
+
return jid;
|
|
80
|
+
const lid = await getLIDForPN(jid);
|
|
81
|
+
return lid ?? jid;
|
|
82
|
+
}
|
|
83
|
+
/** Resolve target JID for issuing privacy token based on AB prop 14303 */
|
|
84
|
+
export async function resolveIssuanceJid(jid, issueToLid, getLIDForPN, getPNForLID) {
|
|
85
|
+
if (issueToLid) {
|
|
86
|
+
if (isLidUser(jid))
|
|
87
|
+
return jid;
|
|
88
|
+
const lid = await getLIDForPN(jid);
|
|
89
|
+
return lid ?? jid;
|
|
90
|
+
}
|
|
91
|
+
if (!isLidUser(jid))
|
|
92
|
+
return jid;
|
|
93
|
+
if (getPNForLID) {
|
|
94
|
+
const pn = await getPNForLID(jid);
|
|
95
|
+
return pn ?? jid;
|
|
96
|
+
}
|
|
97
|
+
return jid;
|
|
98
|
+
}
|
|
99
|
+
export async function buildTcTokenFromJid({ authState, jid, baseContent = [], getLIDForPN }) {
|
|
100
|
+
try {
|
|
101
|
+
const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
|
|
102
|
+
const tcTokenData = await authState.keys.get('tctoken', [storageJid]);
|
|
103
|
+
const entry = tcTokenData?.[storageJid];
|
|
104
|
+
const tcTokenBuffer = entry?.token;
|
|
105
|
+
if (!tcTokenBuffer?.length || isTcTokenExpired(entry?.timestamp)) {
|
|
106
|
+
if (tcTokenBuffer) {
|
|
107
|
+
// Preserve senderTimestamp so shouldSendNewTcToken() keeps its dedupe state
|
|
108
|
+
// after we drop the unusable peer token. Only wipe the record entirely when
|
|
109
|
+
// there's nothing worth keeping.
|
|
110
|
+
const cleared = entry?.senderTimestamp !== undefined
|
|
111
|
+
? { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp }
|
|
112
|
+
: null;
|
|
113
|
+
await authState.keys.set({ tctoken: { [storageJid]: cleared } });
|
|
114
|
+
}
|
|
6
115
|
return baseContent.length > 0 ? baseContent : undefined;
|
|
116
|
+
}
|
|
7
117
|
baseContent.push({
|
|
8
118
|
tag: 'tctoken',
|
|
9
119
|
attrs: {},
|
|
@@ -14,4 +124,40 @@ export async function buildTcTokenFromJid({ authState, jid, baseContent = [] })
|
|
|
14
124
|
catch (error) {
|
|
15
125
|
return baseContent.length > 0 ? baseContent : undefined;
|
|
16
126
|
}
|
|
17
|
-
}
|
|
127
|
+
}
|
|
128
|
+
export async function storeTcTokensFromIqResult({ result, fallbackJid, keys, getLIDForPN, onNewJidStored }) {
|
|
129
|
+
const tokensNode = getBinaryNodeChild(result, 'tokens');
|
|
130
|
+
if (!tokensNode)
|
|
131
|
+
return;
|
|
132
|
+
const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
|
|
133
|
+
for (const tokenNode of tokenNodes) {
|
|
134
|
+
if (tokenNode.attrs.type !== 'trusted_contact' || !(tokenNode.content instanceof Uint8Array)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// In notifications tokenNode.attrs.jid is your own device JID, not the sender's
|
|
138
|
+
const rawJid = jidNormalizedUser(fallbackJid || tokenNode.attrs.jid);
|
|
139
|
+
if (!isRegularUser(rawJid))
|
|
140
|
+
continue;
|
|
141
|
+
const storageJid = await resolveTcTokenJid(rawJid, getLIDForPN);
|
|
142
|
+
const existingTcData = await keys.get('tctoken', [storageJid]);
|
|
143
|
+
const existingEntry = existingTcData[storageJid];
|
|
144
|
+
const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0;
|
|
145
|
+
const incomingTs = tokenNode.attrs.t ? Number(tokenNode.attrs.t) : 0;
|
|
146
|
+
// timestamp-less tokens would be immediately expired
|
|
147
|
+
if (!incomingTs)
|
|
148
|
+
continue;
|
|
149
|
+
if (existingTs > 0 && existingTs > incomingTs)
|
|
150
|
+
continue;
|
|
151
|
+
await keys.set({
|
|
152
|
+
tctoken: {
|
|
153
|
+
[storageJid]: {
|
|
154
|
+
...existingEntry,
|
|
155
|
+
token: Buffer.from(tokenNode.content),
|
|
156
|
+
timestamp: tokenNode.attrs.t
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
onNewJidStored?.(storageJid);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=tc-token-utils.js.map
|