@itsliaaa/baileys 0.1.33 → 0.2.1
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 +109 -58
- package/lib/Defaults/index.js +2 -0
- package/lib/Socket/chats.js +185 -52
- 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 +14 -0
- package/lib/Utils/process-message.js +53 -1
- package/lib/Utils/rich-message-utils.js +1 -1
- 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 +1 -1
|
@@ -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
|
@@ -674,6 +674,7 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
674
674
|
extContent.description = urlInfo.description;
|
|
675
675
|
extContent.title = urlInfo.title;
|
|
676
676
|
extContent.previewType = urlInfo.previewType ?? 0;
|
|
677
|
+
extContent.linkPreviewMetadata = urlInfo.linkPreviewMetadata;
|
|
677
678
|
const img = urlInfo.highQualityThumbnail;
|
|
678
679
|
if (img) {
|
|
679
680
|
extContent.thumbnailDirectPath = img.directPath;
|
|
@@ -685,6 +686,19 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
685
686
|
extContent.thumbnailEncSha256 = img.fileEncSha256;
|
|
686
687
|
}
|
|
687
688
|
}
|
|
689
|
+
let faviconInfo = message.favicon;
|
|
690
|
+
if (faviconInfo) {
|
|
691
|
+
faviconInfo = await prepareWAMessageMedia(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
|
+
}
|
|
688
702
|
if (options.backgroundColor) {
|
|
689
703
|
extContent.backgroundArgb = await assertColor(options.backgroundColor);
|
|
690
704
|
}
|
|
@@ -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,
|
|
@@ -291,7 +291,7 @@ export const prepareRichResponseMessage = (content) => {
|
|
|
291
291
|
}));
|
|
292
292
|
submessages.push({
|
|
293
293
|
messageType: RichSubMessageType.TEXT,
|
|
294
|
-
messageText: linkField.text + `{{${prefix}}}¹{{/${prefix}}}`,
|
|
294
|
+
messageText: linkField.text + ` {{${prefix}}}¹{{/${prefix}}} `,
|
|
295
295
|
inlineEntities: [{
|
|
296
296
|
key: prefix,
|
|
297
297
|
metadata: {
|
|
@@ -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
|
|
@@ -4,27 +4,26 @@ import { proto } from '../../WAProto/index.js';
|
|
|
4
4
|
import { initAuthCreds } from './auth-utils.js';
|
|
5
5
|
import { BufferJSON } from './generics.js';
|
|
6
6
|
import { LRUCache } from 'lru-cache';
|
|
7
|
+
import { Mutex } from 'async-mutex';
|
|
7
8
|
// Lia@Changes 25-03-26 --- Add useSingleFileAuthState with integrated cache
|
|
8
9
|
const FLUSH_TIMEOUT_MS = 3000;
|
|
9
10
|
// Lia@Changes 22-04-26 --- Enhanced useSingleFileAuthState with LRUCache
|
|
10
11
|
export const useSingleFileAuthState = async (fileName) => {
|
|
11
12
|
const cache = new LRUCache({
|
|
12
|
-
max:
|
|
13
|
+
max: 15000,
|
|
13
14
|
ttl: 1000 * DEFAULT_CACHE_TTLS.SIGNAL_STORE,
|
|
14
15
|
updateAgeOnGet: false,
|
|
15
16
|
updateAgeOnHas: false,
|
|
16
17
|
ttlAutopurge: true
|
|
17
18
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (loadPromise) return loadPromise;
|
|
27
|
-
loadPromise = (async () => {
|
|
19
|
+
// Lia@Changes 26-04-26 --- Add mutex for prevent race condition
|
|
20
|
+
const mutex = new Mutex();
|
|
21
|
+
let fileData = {};
|
|
22
|
+
let isLoaded = false;
|
|
23
|
+
let flushTimeout = null;
|
|
24
|
+
const loadKey = async () => {
|
|
25
|
+
return await mutex.runExclusive(async () => {
|
|
26
|
+
if (isLoaded) return;
|
|
28
27
|
try {
|
|
29
28
|
const data = JSON.parse(await readFile(fileName, 'utf-8'), BufferJSON.reviver);
|
|
30
29
|
fileData = data || {};
|
|
@@ -36,26 +35,20 @@ export const useSingleFileAuthState = async (fileName) => {
|
|
|
36
35
|
fileData = {};
|
|
37
36
|
}
|
|
38
37
|
isLoaded = true;
|
|
39
|
-
|
|
40
|
-
})();
|
|
41
|
-
return loadPromise;
|
|
38
|
+
});
|
|
42
39
|
};
|
|
43
40
|
const flushKey = () => {
|
|
44
41
|
if (flushTimeout) return;
|
|
45
42
|
flushTimeout = setTimeout(async () => {
|
|
46
43
|
flushTimeout = null;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await writeFile(tempFile, JSON.stringify(fileData, BufferJSON.replacer));
|
|
56
|
-
await rename(tempFile, fileName);
|
|
57
|
-
} while (isNeedWrite);
|
|
58
|
-
isWriting = false;
|
|
44
|
+
await mutex.runExclusive(async () => {
|
|
45
|
+
try {
|
|
46
|
+
const tempFile = fileName + '.temp';
|
|
47
|
+
await writeFile(tempFile, JSON.stringify(fileData, BufferJSON.replacer));
|
|
48
|
+
await rename(tempFile, fileName);
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
});
|
|
59
52
|
}, FLUSH_TIMEOUT_MS);
|
|
60
53
|
};
|
|
61
54
|
const writeKey = (keyName, value) => {
|
|
@@ -11,11 +11,34 @@ export class USyncContactProtocol {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
getUserElement(user) {
|
|
14
|
-
|
|
14
|
+
if (user.phone) {
|
|
15
|
+
return {
|
|
16
|
+
tag: 'contact',
|
|
17
|
+
attrs: {},
|
|
18
|
+
content: user.phone
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (user.username) {
|
|
22
|
+
return {
|
|
23
|
+
tag: 'contact',
|
|
24
|
+
attrs: {
|
|
25
|
+
username: user.username,
|
|
26
|
+
...(user.usernameKey ? { pin: user.usernameKey } : {}),
|
|
27
|
+
...(user.lid ? { lid: user.lid } : {})
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (user.type) {
|
|
32
|
+
return {
|
|
33
|
+
tag: 'contact',
|
|
34
|
+
attrs: {
|
|
35
|
+
type: user.type
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
15
39
|
return {
|
|
16
40
|
tag: 'contact',
|
|
17
|
-
attrs: {}
|
|
18
|
-
content: user.phone
|
|
41
|
+
attrs: {}
|
|
19
42
|
};
|
|
20
43
|
}
|
|
21
44
|
parser(node) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { assertNodeErrorFree } from '../../WABinary/index.js';
|
|
2
|
+
import { USyncUser } from '../USyncUser.js';
|
|
3
|
+
export class USyncUsernameProtocol {
|
|
4
|
+
name = 'username';
|
|
5
|
+
getQueryElement() {
|
|
6
|
+
return {
|
|
7
|
+
tag: 'username',
|
|
8
|
+
attrs: {}
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
getUserElement(user) {
|
|
12
|
+
void user;
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
parser(node) {
|
|
16
|
+
if (node.tag === 'username') {
|
|
17
|
+
assertNodeErrorFree(node);
|
|
18
|
+
return typeof node.content === 'string' ? node.content : null;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './USyncDeviceProtocol.js';
|
|
2
2
|
export * from './USyncContactProtocol.js';
|
|
3
3
|
export * from './USyncStatusProtocol.js';
|
|
4
|
-
export * from './USyncDisappearingModeProtocol.js';
|
|
4
|
+
export * from './USyncDisappearingModeProtocol.js';
|
|
5
|
+
export * from './USyncUsernameProtocol.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getBinaryNodeChild } from '../WABinary/index.js';
|
|
2
2
|
import { USyncBotProfileProtocol } from './Protocols/UsyncBotProfileProtocol.js';
|
|
3
3
|
import { USyncLIDProtocol } from './Protocols/UsyncLIDProtocol.js';
|
|
4
|
-
import { USyncContactProtocol, USyncDeviceProtocol, USyncDisappearingModeProtocol, USyncStatusProtocol } from './Protocols/index.js';
|
|
4
|
+
import { USyncContactProtocol, USyncDeviceProtocol, USyncDisappearingModeProtocol, USyncStatusProtocol, USyncUsernameProtocol } from './Protocols/index.js';
|
|
5
5
|
import { USyncUser } from './USyncUser.js';
|
|
6
6
|
export class USyncQuery {
|
|
7
7
|
constructor() {
|
|
@@ -90,4 +90,8 @@ export class USyncQuery {
|
|
|
90
90
|
this.protocols.push(new USyncLIDProtocol());
|
|
91
91
|
return this;
|
|
92
92
|
}
|
|
93
|
+
withUsernameProtocol() {
|
|
94
|
+
this.protocols.push(new USyncUsernameProtocol());
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
93
97
|
}
|
package/lib/WAUSync/USyncUser.js
CHANGED
|
@@ -11,6 +11,14 @@ export class USyncUser {
|
|
|
11
11
|
this.phone = phone;
|
|
12
12
|
return this;
|
|
13
13
|
}
|
|
14
|
+
withUsername(username) {
|
|
15
|
+
this.username = username;
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
withUsernameKey(usernameKey) {
|
|
19
|
+
this.usernameKey = usernameKey;
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
14
22
|
withType(type) {
|
|
15
23
|
this.type = type;
|
|
16
24
|
return this;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itsliaaa/baileys",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Enhanced Baileys v7 with fixed newsletter media upload, plus support for interactive messages, albums, and more message types.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"type": "module",
|