@violetix/baileys 0.3.18 → 0.5.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 +174 -0
- package/lib/Defaults/index.js +4 -2
- package/lib/Socket/chats.js +3 -1
- package/lib/Socket/groups.js +8 -3
- package/lib/Socket/index.d.ts +7 -0
- package/lib/Socket/messages-recv.js +3 -3
- package/lib/Socket/messages-send.js +140 -10
- package/lib/Socket/socket.d.ts +6 -0
- package/lib/Socket/socket.js +92 -30
- package/lib/Store/make-in-memory-store.js +32 -24
- package/lib/Utils/a2ui.d.ts +50 -0
- package/lib/Utils/a2ui.js +168 -0
- package/lib/Utils/anchor-guard.d.ts +71 -0
- package/lib/Utils/anchor-guard.js +404 -0
- package/lib/Utils/business.js +25 -11
- package/lib/Utils/chat-utils.js +5 -3
- package/lib/Utils/event-buffer.js +4 -5
- package/lib/Utils/generics.js +21 -6
- package/lib/Utils/ignore-key.d.ts +51 -0
- package/lib/Utils/ignore-key.js +176 -0
- package/lib/Utils/index.d.ts +3 -0
- package/lib/Utils/index.js +3 -0
- package/lib/Utils/messages-media.d.ts +2 -0
- package/lib/Utils/messages-media.js +25 -2
- package/lib/Utils/messages.d.ts +1 -0
- package/lib/Utils/messages.js +74 -1
- package/lib/Utils/process-message.js +4 -1
- package/lib/Utils/rich-message-utils.js +1 -1
- package/lib/Utils/use-multi-file-auth-state.d.ts +2 -1
- package/lib/Utils/use-multi-file-auth-state.js +27 -6
- package/lib/Utils/use-single-file-auth-state.d.ts +1 -1
- package/lib/Utils/use-single-file-auth-state.js +12 -3
- package/package.json +2 -2
package/lib/Socket/socket.js
CHANGED
|
@@ -7,6 +7,8 @@ import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_C
|
|
|
7
7
|
import { QueryIds, ReachoutTimelockEnforcementType } from '../Types/index.js';
|
|
8
8
|
import { DisconnectReason, XWAPaths } from '../Types/index.js';
|
|
9
9
|
import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, buildPairingQRData, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, fetchLatestWaWebVersion, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, getPairingCodePlatform, invalidPairingCodeChars, makeEventBuffer, makeNoiseHandler, normalizePairingCode, PAIRING_CODE_ALPHABET, promiseTimeout, signedKeyPair, xmppSignedPreKey } from '../Utils/index.js';
|
|
10
|
+
import { ignoredStanzaNeedsAck, makeIgnoreKeyRegistry } from '../Utils/ignore-key.js';
|
|
11
|
+
import { buildAckStanza } from '../Utils/stanza-ack.js';
|
|
10
12
|
import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
11
13
|
import { BinaryInfo } from '../WAM/BinaryInfo.js';
|
|
12
14
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
@@ -134,12 +136,13 @@ export const makeSocket = (config) => {
|
|
|
134
136
|
node.attrs.id = generateMessageTag();
|
|
135
137
|
}
|
|
136
138
|
const msgId = node.attrs.id;
|
|
139
|
+
pendingQueries++;
|
|
137
140
|
const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
|
|
138
141
|
const result = waitForMessage(msgId, timeoutMs).catch(reject);
|
|
139
142
|
sendNode(node)
|
|
140
143
|
.then(async () => resolve(await result))
|
|
141
144
|
.catch(reject);
|
|
142
|
-
});
|
|
145
|
+
}).finally(() => { pendingQueries--; });
|
|
143
146
|
if (result && 'tag' in result) {
|
|
144
147
|
assertNodeErrorFree(result);
|
|
145
148
|
}
|
|
@@ -374,6 +377,8 @@ export const makeSocket = (config) => {
|
|
|
374
377
|
const keys = addTransactionCapability(authState.keys, logger, transactionOpts);
|
|
375
378
|
const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync);
|
|
376
379
|
let lastDateRecv;
|
|
380
|
+
/** queries awaiting a reply; keep-alive skips its ping while these are flowing */
|
|
381
|
+
let pendingQueries = 0;
|
|
377
382
|
let epoch = 1;
|
|
378
383
|
let keepAliveReq;
|
|
379
384
|
let qrTimer;
|
|
@@ -560,6 +565,11 @@ export const makeSocket = (config) => {
|
|
|
560
565
|
// Don't throw - allow connection to continue even if pre-key check fails
|
|
561
566
|
}
|
|
562
567
|
};
|
|
568
|
+
const ignoreKeyRegistry = makeIgnoreKeyRegistry({
|
|
569
|
+
getMeId: () => authState.creds.me?.id,
|
|
570
|
+
getMeLid: () => authState.creds.me?.lid,
|
|
571
|
+
logger
|
|
572
|
+
});
|
|
563
573
|
const onMessageReceived = async (data) => {
|
|
564
574
|
await noise.decodeFrame(data, frame => {
|
|
565
575
|
lastDateRecv = new Date();
|
|
@@ -570,6 +580,16 @@ export const makeSocket = (config) => {
|
|
|
570
580
|
if (logger.level === 'trace') {
|
|
571
581
|
logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' });
|
|
572
582
|
}
|
|
583
|
+
// Filters run before every other handler, so a dropped stanza never reaches the
|
|
584
|
+
// decryption or persistence paths. The ack still goes out, otherwise the server
|
|
585
|
+
// keeps re-delivering what the host just asked to ignore.
|
|
586
|
+
if (ignoreKeyRegistry.shouldDrop(frame)) {
|
|
587
|
+
logger.debug({ tag: frame.tag, id: msgId, from: frame.attrs.from }, 'stanza dropped by ignoreKey');
|
|
588
|
+
if (ignoredStanzaNeedsAck(frame.tag)) {
|
|
589
|
+
sendNode(buildAckStanza(frame, undefined, authState.creds.me?.id)).catch(err => logger.error({ err, id: msgId }, 'failed to ack ignored stanza'));
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
573
593
|
anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
|
|
574
594
|
const l0 = frame.tag;
|
|
575
595
|
const l1 = frame.attrs || {};
|
|
@@ -694,48 +714,89 @@ export const makeSocket = (config) => {
|
|
|
694
714
|
ws.off('error', onClose);
|
|
695
715
|
});
|
|
696
716
|
};
|
|
717
|
+
/**
|
|
718
|
+
* Keep-alive, modelled on zapo-js (src/transport/keepalive/WaKeepAlive.ts):
|
|
719
|
+
* - self-rescheduling setTimeout with +-10% jitter (min 250ms), so a fleet of sockets in one
|
|
720
|
+
* process drifts apart instead of pinging in the same tick
|
|
721
|
+
* - at most one ping in flight; a slow pong never stacks more pings on a busy loop
|
|
722
|
+
* - no ping while other queries are pending and data arrived recently: the link is
|
|
723
|
+
* demonstrably alive and the ping would only add queue pressure
|
|
724
|
+
* - "lost" is still decided by inbound silence (interval + grace), and skipped when the
|
|
725
|
+
* timer itself fired late (blocked event loop: pongs may be unread in the socket buffer)
|
|
726
|
+
*/
|
|
697
727
|
const startKeepAliveRequest = () => {
|
|
728
|
+
let timer;
|
|
729
|
+
let stopped = false;
|
|
730
|
+
let pingInFlight = false;
|
|
731
|
+
let lastCheckAt;
|
|
732
|
+
let expectedDelayMs = 0;
|
|
733
|
+
const nextDelay = () => {
|
|
734
|
+
const jitter = Math.max(250, Math.floor(keepAliveIntervalMs * 0.1));
|
|
735
|
+
return Math.max(1000, keepAliveIntervalMs + Math.floor((Math.random() * 2 - 1) * jitter));
|
|
736
|
+
};
|
|
737
|
+
const schedule = (delayMs) => {
|
|
738
|
+
if (stopped) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
expectedDelayMs = delayMs;
|
|
742
|
+
timer = safeSetTimeout(() => {
|
|
743
|
+
timer = undefined;
|
|
744
|
+
cb();
|
|
745
|
+
schedule(nextDelay());
|
|
746
|
+
}, delayMs);
|
|
747
|
+
};
|
|
698
748
|
const cb = () => {
|
|
699
749
|
if (!lastDateRecv) {
|
|
700
750
|
lastDateRecv = new Date();
|
|
701
751
|
}
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
752
|
+
const now = Date.now();
|
|
753
|
+
const sinceRecvMs = now - lastDateRecv.getTime();
|
|
754
|
+
const lagMs = lastCheckAt ? now - lastCheckAt - expectedDelayMs : 0;
|
|
755
|
+
lastCheckAt = now;
|
|
756
|
+
const loopStalled = lagMs > keepAliveGracePeriodMs;
|
|
757
|
+
if (loopStalled) {
|
|
758
|
+
logger.warn({ lagMs, sinceLastRecvMs: sinceRecvMs }, 'event loop stalled; deferring keep-alive timeout');
|
|
705
759
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
attrs: {
|
|
710
|
-
id: generateMessageTag(),
|
|
711
|
-
to: S_WHATSAPP_NET,
|
|
712
|
-
type: 'get',
|
|
713
|
-
xmlns: 'w:p'
|
|
714
|
-
},
|
|
715
|
-
content: [{ tag: 'ping', attrs: {} }]
|
|
716
|
-
}).catch(err => {
|
|
717
|
-
logger.error({ trace: err.stack }, 'error in sending keep alive');
|
|
718
|
-
});
|
|
760
|
+
if (sinceRecvMs > keepAliveIntervalMs + keepAliveGracePeriodMs && !loopStalled) {
|
|
761
|
+
void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
|
|
762
|
+
return;
|
|
719
763
|
}
|
|
720
|
-
|
|
764
|
+
if (!ws.isOpen) {
|
|
721
765
|
logger.warn('keep alive called when WS not open');
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (pingInFlight) {
|
|
769
|
+
logger.trace('keep alive skipped: ping already in flight');
|
|
770
|
+
return;
|
|
722
771
|
}
|
|
772
|
+
if (pendingQueries > 0 && sinceRecvMs < keepAliveGracePeriodMs) {
|
|
773
|
+
logger.trace({ pendingQueries, sinceRecvMs }, 'keep alive skipped: queries pending with recent inbound');
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
pingInFlight = true;
|
|
777
|
+
query({
|
|
778
|
+
tag: 'iq',
|
|
779
|
+
attrs: {
|
|
780
|
+
id: generateMessageTag(),
|
|
781
|
+
to: S_WHATSAPP_NET,
|
|
782
|
+
type: 'get',
|
|
783
|
+
xmlns: 'w:p'
|
|
784
|
+
},
|
|
785
|
+
content: [{ tag: 'ping', attrs: {} }]
|
|
786
|
+
}, keepAliveIntervalMs + keepAliveGracePeriodMs)
|
|
787
|
+
.catch(err => {
|
|
788
|
+
// the verdict stays with inbound silence above; a failed ping alone is not "lost"
|
|
789
|
+
logger.warn({ trace: err.stack }, 'error in sending keep alive');
|
|
790
|
+
})
|
|
791
|
+
.finally(() => { pingInFlight = false; });
|
|
723
792
|
};
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
// used the configured value, so any interval other than 30s produced false connectionLost.
|
|
727
|
-
// The random start offset staggers the fleet - on the shared timer every socket in the
|
|
728
|
-
// process pinged in the same tick, which is a self-inflicted burst at 100+ sessions.
|
|
729
|
-
let pinger;
|
|
730
|
-
const starter = safeSetTimeout(() => {
|
|
731
|
-
cb();
|
|
732
|
-
pinger = safeSetInterval(cb, keepAliveIntervalMs);
|
|
733
|
-
}, Math.floor(Math.random() * (keepAliveIntervalMs / 2)));
|
|
793
|
+
// random first tick staggers sockets that connect together (reconnect storms)
|
|
794
|
+
schedule(Math.floor(Math.random() * (keepAliveIntervalMs / 2)));
|
|
734
795
|
keepAliveReq = {
|
|
735
796
|
cb,
|
|
736
797
|
cancel: () => {
|
|
737
|
-
|
|
738
|
-
clearSafeTimer(
|
|
798
|
+
stopped = true;
|
|
799
|
+
clearSafeTimer(timer);
|
|
739
800
|
}
|
|
740
801
|
};
|
|
741
802
|
};
|
|
@@ -1227,6 +1288,7 @@ export const makeSocket = (config) => {
|
|
|
1227
1288
|
return authState.creds.me;
|
|
1228
1289
|
},
|
|
1229
1290
|
generateMessageTag,
|
|
1291
|
+
ignoreKey: ignoreKeyRegistry.register,
|
|
1230
1292
|
query,
|
|
1231
1293
|
waitForMessage,
|
|
1232
1294
|
waitForSocketOpen,
|
|
@@ -9,7 +9,7 @@ import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
|
9
9
|
import { WAProto } from '../Types/index.js';
|
|
10
10
|
import { LabelAssociationType } from '../Types/LabelAssociation.js';
|
|
11
11
|
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
|
|
12
|
-
import { md5, toNumber, updateMessageWithReceipt, updateMessageWithReaction } from '../Utils/index.js';
|
|
12
|
+
import { BufferJSON, md5, toNumber, updateMessageWithReceipt, updateMessageWithReaction } from '../Utils/index.js';
|
|
13
13
|
import { jidDecode, jidNormalizedUser } from '../WABinary/index.js';
|
|
14
14
|
import { makeOrderedDictionary } from './make-ordered-dictionary.js';
|
|
15
15
|
import { ObjectRepository } from './object-repository.js';
|
|
@@ -69,6 +69,9 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
|
+
// one key rule for every handler; upsert used alt+normalized while update/receipt/reaction/delete
|
|
73
|
+
// used raw or non-alt keys, so LID chats never found their stored messages
|
|
74
|
+
const keyJid = (key) => jidNormalizedUser(key.remoteJidAlt || key.remoteJid);
|
|
72
75
|
const assertMessageList = (jid) => {
|
|
73
76
|
if (!messages[jid]) {
|
|
74
77
|
messages[jid] = makeMessagesDictionary();
|
|
@@ -116,8 +119,7 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
116
119
|
}
|
|
117
120
|
logger.debug({ deletedContacts: isLatest ? oldContacts.size : 0, newContacts }, 'synced contacts');
|
|
118
121
|
for (const msg of newMessages) {
|
|
119
|
-
const
|
|
120
|
-
const list = assertMessageList(jid);
|
|
122
|
+
const list = assertMessageList(keyJid(msg.key));
|
|
121
123
|
list.upsert(msg, 'prepend');
|
|
122
124
|
}
|
|
123
125
|
enforceLimits(messages, groupMetadata, chats, maxChats, maxMessagesPerChat);
|
|
@@ -149,9 +151,13 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
else {
|
|
152
|
-
|
|
154
|
+
logger.debug({ update }, 'got update for non-existant contact');
|
|
155
|
+
continue;
|
|
153
156
|
}
|
|
154
|
-
Object.assign(
|
|
157
|
+
// was Object.assign(contact, contact): update fields (notify, name...) never landed.
|
|
158
|
+
// id may be a WA_ADD_NOTIF hash and imgUrl a sentinel, so neither is copied.
|
|
159
|
+
const { id: _id, imgUrl: _imgUrl, ...fields } = update;
|
|
160
|
+
Object.assign(contact, fields);
|
|
155
161
|
}
|
|
156
162
|
});
|
|
157
163
|
on('chats.upsert', newChats => {
|
|
@@ -211,7 +217,7 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
211
217
|
case 'append':
|
|
212
218
|
case 'notify':
|
|
213
219
|
for (const msg of newMessages) {
|
|
214
|
-
const jid =
|
|
220
|
+
const jid = keyJid(msg.key);
|
|
215
221
|
const list = assertMessageList(jid);
|
|
216
222
|
list.upsert(msg, 'append');
|
|
217
223
|
enforceLimits(messages, groupMetadata, chats, maxChats, maxMessagesPerChat);
|
|
@@ -231,7 +237,12 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
231
237
|
on('messages.update', updates => {
|
|
232
238
|
var _a;
|
|
233
239
|
for (const { update, key } of updates) {
|
|
234
|
-
|
|
240
|
+
// no create: an update for an unknown chat must not add an empty list that evicts real ones
|
|
241
|
+
const list = messages[keyJid(key)];
|
|
242
|
+
if (!list) {
|
|
243
|
+
logger.debug({ update }, 'got update for non-existent message');
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
235
246
|
if (update === null || update === void 0 ? void 0 : update.status) {
|
|
236
247
|
const listStatus = (_a = list.get(key.id)) === null || _a === void 0 ? void 0 : _a.status;
|
|
237
248
|
if (listStatus && (update === null || update === void 0 ? void 0 : update.status) <= listStatus) {
|
|
@@ -252,8 +263,7 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
252
263
|
list === null || list === void 0 ? void 0 : list.clear();
|
|
253
264
|
}
|
|
254
265
|
else {
|
|
255
|
-
const
|
|
256
|
-
const list = messages[jid];
|
|
266
|
+
const list = messages[keyJid(item.keys[0])];
|
|
257
267
|
if (list) {
|
|
258
268
|
const idSet = new Set(item.keys.map(k => k.id));
|
|
259
269
|
list.filter(m => !idSet.has(m.key.id));
|
|
@@ -308,8 +318,7 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
308
318
|
});
|
|
309
319
|
on('message-receipt.update', updates => {
|
|
310
320
|
for (const { key, receipt } of updates) {
|
|
311
|
-
const
|
|
312
|
-
const msg = obj === null || obj === void 0 ? void 0 : obj.get(key.id);
|
|
321
|
+
const msg = messages[keyJid(key)]?.get(key.id);
|
|
313
322
|
if (msg) {
|
|
314
323
|
updateMessageWithReceipt(msg, receipt);
|
|
315
324
|
}
|
|
@@ -317,8 +326,7 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
317
326
|
});
|
|
318
327
|
on('messages.reaction', (reactions) => {
|
|
319
328
|
for (const { key, reaction } of reactions) {
|
|
320
|
-
const
|
|
321
|
-
const msg = obj === null || obj === void 0 ? void 0 : obj.get(key.id);
|
|
329
|
+
const msg = messages[keyJid(key)]?.get(key.id);
|
|
322
330
|
if (msg) {
|
|
323
331
|
updateMessageWithReaction(msg, reaction);
|
|
324
332
|
}
|
|
@@ -363,28 +371,28 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
363
371
|
bind,
|
|
364
372
|
/** loads messages from the store, if not found -- uses the legacy connection */
|
|
365
373
|
loadMessages: async (jid, count, cursor) => {
|
|
366
|
-
const list =
|
|
374
|
+
const list = messages[jid]; // read-only: don't create phantom lists
|
|
367
375
|
const mode = !cursor || 'before' in cursor ? 'before' : 'after';
|
|
368
376
|
const cursorKey = !!cursor ? ('before' in cursor ? cursor.before : cursor.after) : undefined;
|
|
369
|
-
const cursorValue = cursorKey ? list
|
|
370
|
-
let
|
|
377
|
+
const cursorValue = cursorKey ? list?.get(cursorKey.id) : undefined;
|
|
378
|
+
let result;
|
|
371
379
|
if (list && mode === 'before' && (!cursorKey || cursorValue)) {
|
|
372
380
|
if (cursorValue) {
|
|
373
381
|
const msgIdx = list.array.findIndex(m => m.key.id === (cursorKey === null || cursorKey === void 0 ? void 0 : cursorKey.id));
|
|
374
|
-
|
|
382
|
+
result = list.array.slice(0, msgIdx);
|
|
375
383
|
}
|
|
376
384
|
else {
|
|
377
|
-
|
|
385
|
+
result = list.array;
|
|
378
386
|
}
|
|
379
|
-
const diff = count -
|
|
387
|
+
const diff = count - result.length;
|
|
380
388
|
if (diff < 0) {
|
|
381
|
-
|
|
389
|
+
result = result.slice(-count); // get the last X messages
|
|
382
390
|
}
|
|
383
391
|
}
|
|
384
392
|
else {
|
|
385
|
-
|
|
393
|
+
result = [];
|
|
386
394
|
}
|
|
387
|
-
return
|
|
395
|
+
return result;
|
|
388
396
|
},
|
|
389
397
|
/**
|
|
390
398
|
* Get all available labels for profile
|
|
@@ -447,13 +455,13 @@ export const makeInMemoryStore = (config = {}) => {
|
|
|
447
455
|
toJSON,
|
|
448
456
|
fromJSON,
|
|
449
457
|
writeToFile: (path) => {
|
|
450
|
-
writeFileSync(path, JSON.stringify(toJSON()));
|
|
458
|
+
writeFileSync(path, JSON.stringify(toJSON(), BufferJSON.replacer));
|
|
451
459
|
},
|
|
452
460
|
readFromFile: (path) => {
|
|
453
461
|
if (existsSync(path)) {
|
|
454
462
|
logger.debug({ path }, 'reading from file');
|
|
455
463
|
const jsonStr = readFileSync(path, { encoding: 'utf-8' });
|
|
456
|
-
const json = JSON.parse(jsonStr);
|
|
464
|
+
const json = JSON.parse(jsonStr, BufferJSON.reviver);
|
|
457
465
|
fromJSON(json);
|
|
458
466
|
}
|
|
459
467
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type A2UIWidget = {
|
|
2
|
+
uuid: string;
|
|
3
|
+
data: string;
|
|
4
|
+
type: string;
|
|
5
|
+
fallback?: string;
|
|
6
|
+
};
|
|
7
|
+
export type A2UIBuildOptions = {
|
|
8
|
+
uuid?: string;
|
|
9
|
+
surfaceId?: string;
|
|
10
|
+
type?: string;
|
|
11
|
+
/** `false` emits a bare `{ components }` payload instead of the `createSurface` envelope. */
|
|
12
|
+
wrapped?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type A2UIChoice = {
|
|
15
|
+
label: string;
|
|
16
|
+
value: unknown;
|
|
17
|
+
};
|
|
18
|
+
export type A2UIListCardItem = {
|
|
19
|
+
title: string;
|
|
20
|
+
price?: string;
|
|
21
|
+
trailingLabel?: string;
|
|
22
|
+
assetId?: string;
|
|
23
|
+
assetType?: string;
|
|
24
|
+
emphasis?: string;
|
|
25
|
+
};
|
|
26
|
+
/** Every method registers a component and returns its id; container methods take those ids. */
|
|
27
|
+
export declare class A2UI {
|
|
28
|
+
constructor(options?: { catalogId?: string; version?: string });
|
|
29
|
+
text(text: string, options?: { id?: string; variant?: string }): string;
|
|
30
|
+
image(url: string, options?: { id?: string; variant?: string; fit?: string }): string;
|
|
31
|
+
video(url: string, options?: { id?: string }): string;
|
|
32
|
+
checkbox(label: string, options?: { id?: string; value?: boolean }): string;
|
|
33
|
+
textField(label: string, options?: { id?: string; variant?: string }): string;
|
|
34
|
+
button(childId: string, options?: { id?: string; variant?: string; action?: any }): string;
|
|
35
|
+
card(childId: string, options?: { id?: string }): string;
|
|
36
|
+
column(children: string[], options?: { id?: string; justify?: string; align?: string }): string;
|
|
37
|
+
row(children: string[], options?: { id?: string }): string;
|
|
38
|
+
divider(options?: { id?: string }): string;
|
|
39
|
+
choicePicker(label: string, options: A2UIChoice[], extra?: {
|
|
40
|
+
id?: string;
|
|
41
|
+
variant?: string;
|
|
42
|
+
value?: unknown;
|
|
43
|
+
displayStyle?: string;
|
|
44
|
+
filterable?: boolean;
|
|
45
|
+
}): string;
|
|
46
|
+
root(children: string[]): this;
|
|
47
|
+
listCard(input: { title: string; items: A2UIListCardItem[]; fallbackText?: string; uuid?: string }): this;
|
|
48
|
+
build(options?: A2UIBuildOptions): A2UIWidget;
|
|
49
|
+
}
|
|
50
|
+
export declare const buildA2UIWidget: (a2ui: A2UI | A2UIWidget, options?: A2UIBuildOptions) => A2UIWidget;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// A2UI (Agent-to-UI) widget builder. Declarative components — Text, Image, Video, Button,
|
|
2
|
+
// Card, Column, Row, Divider, CheckBox, TextField, ChoicePicker, plus the `listCard` shortcut
|
|
3
|
+
// — compiled into the JSON payload WhatsApp carries in `interactiveMessage.bloksWidget`.
|
|
4
|
+
//
|
|
5
|
+
// Ported from @rennzsync/baileys (MIT).
|
|
6
|
+
//
|
|
7
|
+
// Every builder method registers a component and returns its id; ids are what the container
|
|
8
|
+
// methods take, so a tree is written bottom-up:
|
|
9
|
+
//
|
|
10
|
+
// const ui = new A2UI()
|
|
11
|
+
// const title = ui.text('Hello', { variant: 'h1' })
|
|
12
|
+
// const button = ui.button(ui.text('Press me'), { action: { name: 'noop' } })
|
|
13
|
+
// ui.root([ui.card(ui.column([title, button]))])
|
|
14
|
+
// await sock.sendMessage(jid, { a2ui: ui, text: 'Widget' })
|
|
15
|
+
//
|
|
16
|
+
// This is an internal WhatsApp format: a send can succeed while the recipient's client renders
|
|
17
|
+
// nothing. Verify against your target client build before relying on it.
|
|
18
|
+
import { randomUUID } from 'crypto';
|
|
19
|
+
const DEFAULT_CATALOG_ID = 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json';
|
|
20
|
+
export class A2UI {
|
|
21
|
+
constructor({ catalogId = DEFAULT_CATALOG_ID, version = 'v0.9' } = {}) {
|
|
22
|
+
this._version = version;
|
|
23
|
+
this._catalogId = catalogId;
|
|
24
|
+
this._components = new Map();
|
|
25
|
+
this._counter = 0;
|
|
26
|
+
this._rootChildren = [];
|
|
27
|
+
this._listCardPayload = undefined;
|
|
28
|
+
}
|
|
29
|
+
#nextId(prefix) {
|
|
30
|
+
return `${prefix}_${(this._counter++).toString(36)}`;
|
|
31
|
+
}
|
|
32
|
+
#reg(id, component, extra = {}) {
|
|
33
|
+
id ??= this.#nextId(component.toLowerCase());
|
|
34
|
+
if (this._components.has(id)) {
|
|
35
|
+
throw new Error(`Component id "${id}" already used`);
|
|
36
|
+
}
|
|
37
|
+
this._components.set(id, { id, component, ...extra });
|
|
38
|
+
return id;
|
|
39
|
+
}
|
|
40
|
+
text(text, { id, variant = 'body' } = {}) {
|
|
41
|
+
return this.#reg(id, 'Text', { text, variant });
|
|
42
|
+
}
|
|
43
|
+
image(url, { id, variant, fit = 'cover' } = {}) {
|
|
44
|
+
return this.#reg(id, 'Image', { url, ...(variant ? { variant } : {}), fit });
|
|
45
|
+
}
|
|
46
|
+
video(url, { id } = {}) {
|
|
47
|
+
return this.#reg(id, 'Video', { url });
|
|
48
|
+
}
|
|
49
|
+
checkbox(label, { id, value = false } = {}) {
|
|
50
|
+
return this.#reg(id, 'CheckBox', { label, value });
|
|
51
|
+
}
|
|
52
|
+
textField(label, { id, variant = 'text' } = {}) {
|
|
53
|
+
return this.#reg(id, 'TextField', { label, variant });
|
|
54
|
+
}
|
|
55
|
+
button(childId, { id, variant = 'primary', action } = {}) {
|
|
56
|
+
if (!childId) {
|
|
57
|
+
throw new TypeError('button(childId) requires the id of a child component (e.g. from .text())');
|
|
58
|
+
}
|
|
59
|
+
return this.#reg(id, 'Button', { child: childId, variant, ...(action ? { action } : {}) });
|
|
60
|
+
}
|
|
61
|
+
card(childId, { id } = {}) {
|
|
62
|
+
if (!childId) {
|
|
63
|
+
throw new TypeError('card(childId) requires the id of a child component');
|
|
64
|
+
}
|
|
65
|
+
return this.#reg(id, 'Card', { child: childId });
|
|
66
|
+
}
|
|
67
|
+
column(children = [], { id, justify, align } = {}) {
|
|
68
|
+
if (!children.length) {
|
|
69
|
+
throw new TypeError('column(children) requires at least one child id');
|
|
70
|
+
}
|
|
71
|
+
return this.#reg(id, 'Column', { children, ...(justify ? { justify } : {}), ...(align ? { align } : {}) });
|
|
72
|
+
}
|
|
73
|
+
row(children = [], { id } = {}) {
|
|
74
|
+
if (!children.length) {
|
|
75
|
+
throw new TypeError('row(children) requires at least one child id');
|
|
76
|
+
}
|
|
77
|
+
return this.#reg(id, 'Row', { children });
|
|
78
|
+
}
|
|
79
|
+
divider({ id } = {}) {
|
|
80
|
+
return this.#reg(id, 'Divider', {});
|
|
81
|
+
}
|
|
82
|
+
choicePicker(label, options, { id, variant = 'mutuallyExclusive', value, displayStyle = 'checkbox', filterable = false } = {}) {
|
|
83
|
+
if (!Array.isArray(options) || !options.length) {
|
|
84
|
+
throw new TypeError('choicePicker(label, options) requires a non-empty options array of {label, value}');
|
|
85
|
+
}
|
|
86
|
+
return this.#reg(id, 'ChoicePicker', {
|
|
87
|
+
label,
|
|
88
|
+
variant,
|
|
89
|
+
...(value !== undefined ? { value } : {}),
|
|
90
|
+
options,
|
|
91
|
+
displayStyle,
|
|
92
|
+
filterable
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Names the top-level components. Required before `build()` unless `listCard` was used. */
|
|
96
|
+
root(children) {
|
|
97
|
+
if (!Array.isArray(children) || !children.length) {
|
|
98
|
+
throw new TypeError('root(children) requires a non-empty array of top-level component ids');
|
|
99
|
+
}
|
|
100
|
+
this._rootChildren = children;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Shortcut for the product-list widget, which uses its own payload shape rather than the
|
|
105
|
+
* component catalog. It replaces whatever the component methods built — `build()` returns
|
|
106
|
+
* this payload and ignores `root()`.
|
|
107
|
+
*/
|
|
108
|
+
listCard({ title, items, fallbackText, uuid = randomUUID() } = {}) {
|
|
109
|
+
if (!title) {
|
|
110
|
+
throw new TypeError('listCard requires a title');
|
|
111
|
+
}
|
|
112
|
+
if (!Array.isArray(items) || !items.length) {
|
|
113
|
+
throw new TypeError('listCard requires a non-empty items array');
|
|
114
|
+
}
|
|
115
|
+
this._listCardPayload = {
|
|
116
|
+
uuid,
|
|
117
|
+
data: JSON.stringify({
|
|
118
|
+
type: 'list_card',
|
|
119
|
+
title,
|
|
120
|
+
fallback_text: fallbackText ?? '',
|
|
121
|
+
items: items.map(item => ({
|
|
122
|
+
asset_id: item.assetId ?? randomUUID().replace(/-/g, '').slice(0, 17),
|
|
123
|
+
asset_type: item.assetType ?? 'PRODUCT_ITEM',
|
|
124
|
+
title: item.title,
|
|
125
|
+
trailing_label: item.price ?? item.trailingLabel ?? '',
|
|
126
|
+
trailing_emphasis: item.emphasis ?? 'strong'
|
|
127
|
+
}))
|
|
128
|
+
}),
|
|
129
|
+
type: 'im_a2ui',
|
|
130
|
+
fallback: fallbackText ?? ''
|
|
131
|
+
};
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
/** Compiles the tree into the `bloksWidget` payload. Called for you by `sendMessage`. */
|
|
135
|
+
build({ uuid = randomUUID(), surfaceId, type = 'im_a2ui', wrapped = true } = {}) {
|
|
136
|
+
if (this._listCardPayload) {
|
|
137
|
+
return this._listCardPayload;
|
|
138
|
+
}
|
|
139
|
+
if (!this._rootChildren.length) {
|
|
140
|
+
throw new Error('Call root([...ids]) before build()');
|
|
141
|
+
}
|
|
142
|
+
const root = { id: 'root', component: 'Column', children: this._rootChildren };
|
|
143
|
+
const components = [root, ...this._components.values()];
|
|
144
|
+
const data = wrapped
|
|
145
|
+
? {
|
|
146
|
+
version: this._version,
|
|
147
|
+
createSurface: {
|
|
148
|
+
surfaceId: surfaceId ?? `starcore-widget=${uuid}`,
|
|
149
|
+
catalogId: this._catalogId,
|
|
150
|
+
components
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
: { components };
|
|
154
|
+
return {
|
|
155
|
+
uuid,
|
|
156
|
+
data: JSON.stringify(data),
|
|
157
|
+
type
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Accepts an `A2UI` instance or an already-built payload, and returns the payload. */
|
|
162
|
+
export const buildA2UIWidget = (a2ui, options = {}) => {
|
|
163
|
+
const widget = typeof a2ui?.build === 'function' ? a2ui.build(options) : a2ui;
|
|
164
|
+
if (!widget || typeof widget.data !== 'string') {
|
|
165
|
+
throw new TypeError('a2ui must be an A2UI instance or a built widget payload with a `data` string');
|
|
166
|
+
}
|
|
167
|
+
return widget;
|
|
168
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type AnchorGuardLimits = {
|
|
2
|
+
maxText: number;
|
|
3
|
+
maxCaption: number;
|
|
4
|
+
maxMentions: number;
|
|
5
|
+
maxGroupMentions: number;
|
|
6
|
+
maxButtons: number;
|
|
7
|
+
maxSections: number;
|
|
8
|
+
maxRows: number;
|
|
9
|
+
maxCards: number;
|
|
10
|
+
maxDepth: number;
|
|
11
|
+
maxNodes: number;
|
|
12
|
+
maxBytes: number;
|
|
13
|
+
maxParamsJson: number;
|
|
14
|
+
maxInvisibleRun: number;
|
|
15
|
+
maxCombiningRun: number;
|
|
16
|
+
maxNewlines: number;
|
|
17
|
+
};
|
|
18
|
+
export declare const ANCHORGUARD_DEFAULTS: AnchorGuardLimits;
|
|
19
|
+
export type AnchorGuardDetectOptions = Partial<AnchorGuardLimits> & {
|
|
20
|
+
/** Pre-computed encoded size, checked against `maxBytes` without re-encoding. */
|
|
21
|
+
byteLength?: number;
|
|
22
|
+
/** Pass the `proto` namespace to also measure the encoded size. Costs an encode per check. */
|
|
23
|
+
proto?: any;
|
|
24
|
+
};
|
|
25
|
+
export type AnchorGuardDetectResult = {
|
|
26
|
+
flagged: boolean;
|
|
27
|
+
reasons: string[];
|
|
28
|
+
};
|
|
29
|
+
export declare const detectBug: (message: any, options?: AnchorGuardDetectOptions) => AnchorGuardDetectResult;
|
|
30
|
+
export type AnchorGuardDetection = {
|
|
31
|
+
direction: 'incoming' | 'outgoing' | 'group-add';
|
|
32
|
+
jid?: string;
|
|
33
|
+
sender?: string;
|
|
34
|
+
message?: any;
|
|
35
|
+
content?: any;
|
|
36
|
+
reasons: string[];
|
|
37
|
+
burst?: boolean;
|
|
38
|
+
};
|
|
39
|
+
export type AnchorGuardOptions = {
|
|
40
|
+
/** Delete a flagged inbound message. Default `true`. */
|
|
41
|
+
autoDelete?: boolean;
|
|
42
|
+
/** `'auto'` revokes own messages and deletes others locally. Default `'auto'`. */
|
|
43
|
+
deleteMode?: 'auto' | 'everyone' | 'me';
|
|
44
|
+
/** Guard `messages.upsert`. Default `true`. */
|
|
45
|
+
guardIncoming?: boolean;
|
|
46
|
+
/** Wrap `sock.sendMessage` and reject crash payloads. Default `false`. */
|
|
47
|
+
guardOutgoing?: boolean;
|
|
48
|
+
/** Block the sender of a flagged message. Default `false`. */
|
|
49
|
+
blockOnBug?: boolean;
|
|
50
|
+
/** Only guard own chats. Default `false`. */
|
|
51
|
+
selfOnly?: boolean;
|
|
52
|
+
thresholds?: Partial<AnchorGuardLimits>;
|
|
53
|
+
onDetect?: (detection: AnchorGuardDetection) => void | Promise<void>;
|
|
54
|
+
proto?: any;
|
|
55
|
+
/** Messages from one sender inside `burstWindowMs` before it counts as a burst. `0` disables. */
|
|
56
|
+
burstThreshold?: number;
|
|
57
|
+
burstWindowMs?: number;
|
|
58
|
+
/** Remove the sender from the group on burst. Needs admin. Default `false`. */
|
|
59
|
+
kickOnBurst?: boolean;
|
|
60
|
+
/** Flag/kick when a burst-flagged inviter adds this account to a group. Default `false`. */
|
|
61
|
+
guardGroupAdds?: boolean;
|
|
62
|
+
/** Guard Meta AI's numbers instead of exempting them. Default `false`. */
|
|
63
|
+
metaAiNumbers?: boolean;
|
|
64
|
+
ownJid?: string;
|
|
65
|
+
logger?: any;
|
|
66
|
+
};
|
|
67
|
+
export declare const createAnchorGuard: (sock: any, options?: AnchorGuardOptions) => {
|
|
68
|
+
detect: (message: any) => AnchorGuardDetectResult;
|
|
69
|
+
stop: () => void;
|
|
70
|
+
};
|
|
71
|
+
export declare const createAntiBugGuard: typeof createAnchorGuard;
|