@langitdeveloper/baileys 2.1.9 → 2.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/lib/Socket/business.js +50 -0
- package/lib/Socket/chats.js +87 -0
- package/lib/Socket/dugong.js +21 -0
- package/lib/Socket/messages-send.js +24 -4
- package/lib/Socket/socket.js +3 -1
- package/lib/Utils/bot-toolkit.js +309 -1
- package/lib/Utils/decode-wa-message.js +39 -3
- package/lib/Utils/generics.js +8 -0
- package/lib/Utils/messages.js +1 -366
- package/lib/Utils/use-multi-file-auth-state.js +6 -1
- package/package.json +1 -1
package/lib/Socket/business.js
CHANGED
|
@@ -246,8 +246,58 @@ const makeBusinessSocket = (config) => {
|
|
|
246
246
|
deleted: +((productCatalogDelNode === null || productCatalogDelNode === void 0 ? void 0 : productCatalogDelNode.attrs.deleted_count) || 0)
|
|
247
247
|
};
|
|
248
248
|
};
|
|
249
|
+
/** updates the business profile fields (address/email/description/websites/hours) */
|
|
250
|
+
const updateBussinesProfile = async (args) => {
|
|
251
|
+
const node = [];
|
|
252
|
+
const simpleFields = ['address', 'email', 'description'];
|
|
253
|
+
node.push(...simpleFields
|
|
254
|
+
.filter((key) => args[key])
|
|
255
|
+
.map((key) => ({ tag: key, attrs: {}, content: args[key] })));
|
|
256
|
+
if (args.websites) {
|
|
257
|
+
node.push(...args.websites.map((website) => ({
|
|
258
|
+
tag: 'website',
|
|
259
|
+
attrs: {},
|
|
260
|
+
content: website
|
|
261
|
+
})));
|
|
262
|
+
}
|
|
263
|
+
if (args.hours) {
|
|
264
|
+
node.push({
|
|
265
|
+
tag: 'business_hours',
|
|
266
|
+
attrs: { timezone: args.hours.timezone },
|
|
267
|
+
content: args.hours.days.map((dayConfig) => {
|
|
268
|
+
const base = {
|
|
269
|
+
tag: 'business_hours_config',
|
|
270
|
+
attrs: { day_of_week: dayConfig.day, mode: dayConfig.mode }
|
|
271
|
+
};
|
|
272
|
+
if (dayConfig.mode === 'specific_hours') {
|
|
273
|
+
return {
|
|
274
|
+
...base,
|
|
275
|
+
attrs: {
|
|
276
|
+
...base.attrs,
|
|
277
|
+
open_time: dayConfig.openTimeInMinutes,
|
|
278
|
+
close_time: dayConfig.closeTimeInMinutes
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return base;
|
|
283
|
+
})
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return await query({
|
|
287
|
+
tag: 'iq',
|
|
288
|
+
attrs: { to: WABinary_1.S_WHATSAPP_NET, type: 'set', xmlns: 'w:biz' },
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
tag: 'business_profile',
|
|
292
|
+
attrs: { v: '3', mutation_type: 'delta' },
|
|
293
|
+
content: node
|
|
294
|
+
}
|
|
295
|
+
]
|
|
296
|
+
});
|
|
297
|
+
};
|
|
249
298
|
return {
|
|
250
299
|
...sock,
|
|
300
|
+
updateBussinesProfile,
|
|
251
301
|
logger: config.logger,
|
|
252
302
|
getOrderDetails,
|
|
253
303
|
getCatalog,
|
package/lib/Socket/chats.js
CHANGED
|
@@ -591,6 +591,10 @@ const makeChatsSocket = (config) => {
|
|
|
591
591
|
const profilePictureUrl = async (jid, type = 'preview', timeoutMs) => {
|
|
592
592
|
var _a;
|
|
593
593
|
jid = (0, WABinary_1.jidNormalizedUser)(jid);
|
|
594
|
+
if ((0, WABinary_1.isJidNewsLetter)(jid)) {
|
|
595
|
+
const metadata = await sock.newsletterMetadata('JID', jid);
|
|
596
|
+
return (metadata === null || metadata === void 0 ? void 0 : metadata.picture) ? (0, Utils_1.getUrlFromDirectPath)(metadata.picture) : undefined;
|
|
597
|
+
}
|
|
594
598
|
const result = await query({
|
|
595
599
|
tag: 'iq',
|
|
596
600
|
attrs: {
|
|
@@ -962,8 +966,91 @@ const makeChatsSocket = (config) => {
|
|
|
962
966
|
}
|
|
963
967
|
}
|
|
964
968
|
});
|
|
969
|
+
/** resolves the LID jid for a regular phone-number jid via a USync query */
|
|
970
|
+
const getLidUser = async (jid) => {
|
|
971
|
+
if (!jid) {
|
|
972
|
+
throw new boom_1.Boom('Please input a jid user');
|
|
973
|
+
}
|
|
974
|
+
if (!(0, WABinary_1.isJidUser)(jid)) {
|
|
975
|
+
throw new boom_1.Boom('Invalid JID: Not a user JID!');
|
|
976
|
+
}
|
|
977
|
+
const targetJid = (0, WABinary_1.jidNormalizedUser)(jid);
|
|
978
|
+
const usyncQuery = new WAUSync_1.USyncQuery().withLIDProtocol().withUser(new WAUSync_1.USyncUser().withId(targetJid));
|
|
979
|
+
const result = await sock.executeUSyncQuery(usyncQuery);
|
|
980
|
+
return result?.list?.[0]?.lid || null;
|
|
981
|
+
};
|
|
982
|
+
/** fetches per-jid disappearing-message duration via USync (so you know it without opening the chat) */
|
|
983
|
+
const fetchDisappearingDuration = async (...jids) => {
|
|
984
|
+
const usyncQuery = new WAUSync_1.USyncQuery().withDisappearingModeProtocol();
|
|
985
|
+
for (const jid of jids) {
|
|
986
|
+
usyncQuery.withUser(new WAUSync_1.USyncUser().withId(jid));
|
|
987
|
+
}
|
|
988
|
+
const result = await sock.executeUSyncQuery(usyncQuery);
|
|
989
|
+
return result ? result.list : undefined;
|
|
990
|
+
};
|
|
991
|
+
/** creates a WhatsApp call link (audio or video) you can share/send like any URL */
|
|
992
|
+
const createCallLink = async (type, event, timeoutMs) => {
|
|
993
|
+
const callType = type?.toLowerCase();
|
|
994
|
+
if (!callType || (callType !== 'audio' && callType !== 'video')) {
|
|
995
|
+
throw new Error('Make sure the type is audio or video!');
|
|
996
|
+
}
|
|
997
|
+
const result = await query({
|
|
998
|
+
tag: 'call',
|
|
999
|
+
attrs: { id: generateMessageTag(), to: '@call' },
|
|
1000
|
+
content: [
|
|
1001
|
+
{
|
|
1002
|
+
tag: 'link_create',
|
|
1003
|
+
attrs: { media: callType },
|
|
1004
|
+
content: event ? [{ tag: 'event', attrs: {}, content: JSON.stringify(event) }] : undefined
|
|
1005
|
+
}
|
|
1006
|
+
]
|
|
1007
|
+
}, timeoutMs);
|
|
1008
|
+
const linkCreateNode = (0, WABinary_1.getBinaryNodeChild)(result, 'link_create');
|
|
1009
|
+
const linkNode = (0, WABinary_1.getBinaryNodeChild)(linkCreateNode, 'link');
|
|
1010
|
+
return linkNode ? (0, WABinary_1.getBinaryNodeChildString)(linkCreateNode, 'link') || linkNode?.content?.toString() : undefined;
|
|
1011
|
+
};
|
|
1012
|
+
/** fetches the list of official WA AI bots available (Meta AI etc.) */
|
|
1013
|
+
const getBotListV2 = async () => {
|
|
1014
|
+
const resp = await query({
|
|
1015
|
+
tag: 'iq',
|
|
1016
|
+
attrs: { xmlns: 'bot', to: WABinary_1.S_WHATSAPP_NET, type: 'get' },
|
|
1017
|
+
content: [{ tag: 'bot', attrs: { v: '2' } }]
|
|
1018
|
+
});
|
|
1019
|
+
const botNode = (0, WABinary_1.getBinaryNodeChild)(resp, 'bot');
|
|
1020
|
+
const botList = [];
|
|
1021
|
+
for (const section of (0, WABinary_1.getBinaryNodeChildren)(botNode, 'section')) {
|
|
1022
|
+
if (section.attrs.type === 'all') {
|
|
1023
|
+
for (const bot of (0, WABinary_1.getBinaryNodeChildren)(section, 'bot')) {
|
|
1024
|
+
botList.push({ jid: bot.attrs.jid, personaId: bot.attrs['persona_id'] });
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return botList;
|
|
1029
|
+
};
|
|
1030
|
+
const updateMessagesPrivacy = async (value) => { await privacyQuery('messages', value); };
|
|
1031
|
+
const updateCallPrivacy = async (value) => { await privacyQuery('calladd', value); };
|
|
1032
|
+
const updateDisableLinkPreviewsPrivacy = (isPreviewsDisabled) => chatModify({ disableLinkPreviews: { isPreviewsDisabled } }, '');
|
|
1033
|
+
const addOrEditContact = (jid, contact) => chatModify({ contact }, jid);
|
|
1034
|
+
const removeContact = (jid) => chatModify({ contact: null }, jid);
|
|
1035
|
+
const addLabel = (jid, labels) => chatModify({ addLabel: { ...labels } }, jid);
|
|
1036
|
+
const clearMessage = (jid, key, timeStamp) => chatModify({ delete: true, lastMessages: [{ key, messageTimestamp: timeStamp }] }, jid);
|
|
1037
|
+
const addOrEditQuickReply = (quickReply) => chatModify({ quickReply }, '');
|
|
1038
|
+
const removeQuickReply = (timestamp) => chatModify({ quickReply: { timestamp, deleted: true } }, '');
|
|
965
1039
|
return {
|
|
966
1040
|
...sock,
|
|
1041
|
+
getLidUser,
|
|
1042
|
+
fetchDisappearingDuration,
|
|
1043
|
+
createCallLink,
|
|
1044
|
+
getBotListV2,
|
|
1045
|
+
updateMessagesPrivacy,
|
|
1046
|
+
updateCallPrivacy,
|
|
1047
|
+
updateDisableLinkPreviewsPrivacy,
|
|
1048
|
+
addOrEditContact,
|
|
1049
|
+
removeContact,
|
|
1050
|
+
addLabel,
|
|
1051
|
+
clearMessage,
|
|
1052
|
+
addOrEditQuickReply,
|
|
1053
|
+
removeQuickReply,
|
|
967
1054
|
processingMutex,
|
|
968
1055
|
fetchPrivacySettings,
|
|
969
1056
|
upsertMessage,
|
package/lib/Socket/dugong.js
CHANGED
|
@@ -632,6 +632,27 @@ class kikyy {
|
|
|
632
632
|
|
|
633
633
|
return msg;
|
|
634
634
|
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Sends multiple status updates as a "slide" sequence to the same
|
|
638
|
+
* audience, one after another - e.g. sendStatusAlbum([{ image: {...} },
|
|
639
|
+
* { video: {...} }, { text: 'last slide' }], jids). Returns the array
|
|
640
|
+
* of sent message objects, in the same order they were sent.
|
|
641
|
+
*/
|
|
642
|
+
async sendStatusAlbum(contents, jids = [], delayMs = 1200) {
|
|
643
|
+
if (!Array.isArray(contents) || !contents.length) {
|
|
644
|
+
throw new Error('sendStatusAlbum: contents must be a non-empty array');
|
|
645
|
+
}
|
|
646
|
+
const results = [];
|
|
647
|
+
for (let i = 0; i < contents.length; i++) {
|
|
648
|
+
const sent = await this.sendStatusWhatsApp(contents[i], jids);
|
|
649
|
+
results.push(sent);
|
|
650
|
+
if (i < contents.length - 1) {
|
|
651
|
+
await Utils_1.delay(delayMs);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return results;
|
|
655
|
+
}
|
|
635
656
|
}
|
|
636
657
|
|
|
637
658
|
module.exports = kikyy;
|
|
@@ -261,6 +261,7 @@ const makeMessagesSocket = (config) => {
|
|
|
261
261
|
};
|
|
262
262
|
const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, cachedGroupMetadata, useCachedGroupMetadata, statusJidList, AI = true }) => {
|
|
263
263
|
const meId = authState.creds.me.id;
|
|
264
|
+
const meLid = authState.creds.me.lid;
|
|
264
265
|
let shouldIncludeDeviceIdentity = false;
|
|
265
266
|
let didPushAdditional = false
|
|
266
267
|
const { user, server } = WABinary_1.jidDecode(jid);
|
|
@@ -340,11 +341,30 @@ const makeMessagesSocket = (config) => {
|
|
|
340
341
|
meId,
|
|
341
342
|
});
|
|
342
343
|
const senderKeyJids = [];
|
|
344
|
+
const myRecipients = [];
|
|
345
|
+
const peerRecipients = [];
|
|
346
|
+
const { user: selfPnUser } = WABinary_1.jidDecode(meId);
|
|
347
|
+
const { user: selfLidUser } = meLid ? WABinary_1.jidDecode(meLid) : { user: null };
|
|
343
348
|
for (const { user, device } of devices) {
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
349
|
+
const targetJid = WABinary_1.jidEncode(user, (groupData === null || groupData === void 0 ? void 0 : groupData.addressingMode) === 'lid' ? 'lid' : 's.whatsapp.net', device);
|
|
350
|
+
// a device never needs its own sender-key re-sent to it - it already has it
|
|
351
|
+
const isSendingDeviceItself = targetJid === meId || (meLid && targetJid === meLid);
|
|
352
|
+
if (isSendingDeviceItself) {
|
|
353
|
+
logger.debug({ targetJid, meId, meLid }, 'skipping sender\'s own device for sender-key distribution');
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const belongsToSelf = user === selfPnUser || user === selfLidUser;
|
|
357
|
+
// when this call targets one specific participant (a retry resend),
|
|
358
|
+
// our own other devices don't need it - they're already in sync via
|
|
359
|
+
// the normal multi-device session, only the actual peer does
|
|
360
|
+
const isSkippedAsOwnDeviceDuringTargetedResend = !!participant && !(0, WABinary_1.isJidGroup)(targetJid) && !isStatus && belongsToSelf;
|
|
361
|
+
if (isSkippedAsOwnDeviceDuringTargetedResend) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
(belongsToSelf ? myRecipients : peerRecipients).push(targetJid);
|
|
365
|
+
if (!senderKeyMap[targetJid] || !!participant) {
|
|
366
|
+
senderKeyJids.push(targetJid);
|
|
367
|
+
senderKeyMap[targetJid] = true;
|
|
348
368
|
}
|
|
349
369
|
}
|
|
350
370
|
if (senderKeyJids.length) {
|
package/lib/Socket/socket.js
CHANGED
|
@@ -690,7 +690,7 @@ const makeSocket = (config) => {
|
|
|
690
690
|
if (printQRInTerminal) {
|
|
691
691
|
(0, Utils_1.printQRIfNecessaryListener)(ev, logger);
|
|
692
692
|
}
|
|
693
|
-
|
|
693
|
+
const baseSocket = {
|
|
694
694
|
type: 'md',
|
|
695
695
|
ws,
|
|
696
696
|
ev,
|
|
@@ -718,6 +718,8 @@ const makeSocket = (config) => {
|
|
|
718
718
|
waitForConnectionUpdate: (0, Utils_1.bindWaitForConnectionUpdate)(ev),
|
|
719
719
|
sendWAMBuffer,
|
|
720
720
|
};
|
|
721
|
+
Object.assign(baseSocket, (0, Utils_1.makeBotToolkit)(baseSocket, logger));
|
|
722
|
+
return baseSocket;
|
|
721
723
|
};
|
|
722
724
|
exports.makeSocket = makeSocket;
|
|
723
725
|
/**
|
package/lib/Utils/bot-toolkit.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.makeBotToolkit = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A grab-bag of small, self-contained quality-of-life features that don't
|
|
6
|
+
* exist in upstream Baileys or other forks. None of this touches the core
|
|
7
|
+
* decrypt/encrypt/socket pipeline - it's all additive, attached to the
|
|
8
|
+
* socket's return object.
|
|
9
|
+
*/
|
|
4
10
|
const makeBotToolkit = (conn, logger) => {
|
|
5
11
|
const startedAt = Date.now();
|
|
6
12
|
const seenMessageIds = new Map(); // id -> timestamp, used for dedup
|
|
@@ -17,7 +23,278 @@ const makeBotToolkit = (conn, logger) => {
|
|
|
17
23
|
}
|
|
18
24
|
}
|
|
19
25
|
};
|
|
26
|
+
const pollTallies = new Map(); // pollMsgId -> { question, options, votes: Map<voterJid, optionIndex[]>, listening }
|
|
27
|
+
const groupMetaCache = new Map(); // jid -> { data, fetchedAt }
|
|
28
|
+
const GROUP_META_TTL_MS = 60 * 1000;
|
|
20
29
|
return {
|
|
30
|
+
/**
|
|
31
|
+
* Checks if a user is an admin/superadmin in a group, using the
|
|
32
|
+
* cached metadata getter above so repeated checks (every message in
|
|
33
|
+
* a busy group) don't keep re-fetching from WA.
|
|
34
|
+
*/
|
|
35
|
+
async isGroupAdmin(groupJid, userJid) {
|
|
36
|
+
const meta = await this.getCachedGroupMetadata(groupJid);
|
|
37
|
+
const participant = meta?.participants?.find((p) => p.id === userJid || p.jid === userJid);
|
|
38
|
+
return participant?.admin === 'admin' || participant?.admin === 'superadmin';
|
|
39
|
+
},
|
|
40
|
+
/**
|
|
41
|
+
* Splits long text into WhatsApp-safe chunks and sends them one
|
|
42
|
+
* after another (with a small delay), so a long AI response or log
|
|
43
|
+
* dump doesn't get truncated or rejected for being too long.
|
|
44
|
+
*/
|
|
45
|
+
async sendChunked(jid, text, options = {}, maxLen = 4000, delayMs = 800) {
|
|
46
|
+
if (!text || text.length <= maxLen) {
|
|
47
|
+
return [await conn.sendMessage(jid, { text, ...options })];
|
|
48
|
+
}
|
|
49
|
+
const chunks = [];
|
|
50
|
+
for (let i = 0; i < text.length; i += maxLen) {
|
|
51
|
+
chunks.push(text.slice(i, i + maxLen));
|
|
52
|
+
}
|
|
53
|
+
const sent = [];
|
|
54
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
55
|
+
sent.push(await conn.sendMessage(jid, { text: chunks[i], ...options }));
|
|
56
|
+
if (i < chunks.length - 1) {
|
|
57
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return sent;
|
|
61
|
+
},
|
|
62
|
+
/**
|
|
63
|
+
* Shows "typing..." presence for a bit before actually sending - makes
|
|
64
|
+
* the bot feel less robotic. `typingMs` is how long to show typing
|
|
65
|
+
* before the message goes out.
|
|
66
|
+
*/
|
|
67
|
+
async sendWithTyping(jid, content, options = {}, typingMs = 1200) {
|
|
68
|
+
try {
|
|
69
|
+
await conn.sendPresenceUpdate('composing', jid);
|
|
70
|
+
await new Promise((r) => setTimeout(r, typingMs));
|
|
71
|
+
await conn.sendPresenceUpdate('paused', jid);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
logger.debug({ err }, 'sendWithTyping: presence update failed, sending anyway');
|
|
75
|
+
}
|
|
76
|
+
return conn.sendMessage(jid, content, options);
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* sendMessage with automatic retry on transient failures (network
|
|
80
|
+
* blips, rate limiting) - NOT for permanent failures like invalid
|
|
81
|
+
* jid. Retries up to `retries` times with growing delay.
|
|
82
|
+
*/
|
|
83
|
+
async sendMessageSafe(jid, content, options = {}, retries = 3) {
|
|
84
|
+
let lastErr;
|
|
85
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
86
|
+
try {
|
|
87
|
+
return await conn.sendMessage(jid, content, options);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
lastErr = err;
|
|
91
|
+
const isLikelyTransient = /(timed out|ECONNRESET|ETIMEDOUT|rate-overlimit|Internal Server Error)/i.test(err?.message || '');
|
|
92
|
+
if (!isLikelyTransient || attempt === retries) {
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
logger.debug({ attempt, err }, 'sendMessageSafe: transient failure, retrying');
|
|
96
|
+
await new Promise((r) => setTimeout(r, 1000 * attempt));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
throw lastErr;
|
|
100
|
+
},
|
|
101
|
+
/**
|
|
102
|
+
* Downloads any URL into a Buffer - the one-liner you end up writing
|
|
103
|
+
* in every plugin that needs to grab an image/file from the internet
|
|
104
|
+
* before sending it.
|
|
105
|
+
*/
|
|
106
|
+
async getBuffer(url, opts = {}) {
|
|
107
|
+
const res = await fetch(url, opts);
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
throw new Error(`getBuffer: HTTP ${res.status} fetching ${url}`);
|
|
110
|
+
}
|
|
111
|
+
const arrBuf = await res.arrayBuffer();
|
|
112
|
+
return Buffer.from(arrBuf);
|
|
113
|
+
},
|
|
114
|
+
/**
|
|
115
|
+
* Downloads a URL and sends it as the right message type automatically,
|
|
116
|
+
* based on the response's content-type (falls back to sniffing the
|
|
117
|
+
* file extension in the URL if the server doesn't send one).
|
|
118
|
+
* await conn.sendFileFromUrl(jid, 'https://example.com/cat.png', { caption: 'meow' })
|
|
119
|
+
*/
|
|
120
|
+
async sendFileFromUrl(jid, url, options = {}) {
|
|
121
|
+
const res = await fetch(url);
|
|
122
|
+
if (!res.ok) {
|
|
123
|
+
throw new Error(`sendFileFromUrl: HTTP ${res.status} fetching ${url}`);
|
|
124
|
+
}
|
|
125
|
+
const contentType = res.headers.get('content-type') || '';
|
|
126
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
127
|
+
let contentKey = 'document';
|
|
128
|
+
if (contentType.startsWith('image/') || /\.(jpe?g|png|webp|gif)$/i.test(url)) {
|
|
129
|
+
contentKey = 'image';
|
|
130
|
+
}
|
|
131
|
+
else if (contentType.startsWith('video/') || /\.(mp4|mkv|mov)$/i.test(url)) {
|
|
132
|
+
contentKey = 'video';
|
|
133
|
+
}
|
|
134
|
+
else if (contentType.startsWith('audio/') || /\.(mp3|ogg|wav|m4a)$/i.test(url)) {
|
|
135
|
+
contentKey = 'audio';
|
|
136
|
+
}
|
|
137
|
+
const content = { [contentKey]: buffer, ...options };
|
|
138
|
+
if (contentKey === 'document' && !content.mimetype) {
|
|
139
|
+
content.mimetype = contentType || 'application/octet-stream';
|
|
140
|
+
}
|
|
141
|
+
return conn.sendMessage(jid, content, options.messageOptions || {});
|
|
142
|
+
},
|
|
143
|
+
/** human-readable uptime string, e.g. "2h 14m 9s", for status/.ping commands */
|
|
144
|
+
uptimeString() {
|
|
145
|
+
const ms = Date.now() - startedAt;
|
|
146
|
+
const s = Math.floor(ms / 1000) % 60;
|
|
147
|
+
const m = Math.floor(ms / 60000) % 60;
|
|
148
|
+
const h = Math.floor(ms / 3600000);
|
|
149
|
+
return `${h}h ${m}m ${s}s`;
|
|
150
|
+
},
|
|
151
|
+
/**
|
|
152
|
+
* Cached groupMetadata - avoids hammering WA's servers when you call
|
|
153
|
+
* groupMetadata() repeatedly for the same group in a short window
|
|
154
|
+
* (e.g. every message handler checking admin status). Falls back to
|
|
155
|
+
* a real fetch automatically once the cache entry goes stale.
|
|
156
|
+
*/
|
|
157
|
+
async getCachedGroupMetadata(jid, ttlMs = GROUP_META_TTL_MS) {
|
|
158
|
+
const cached = groupMetaCache.get(jid);
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
if (cached && now - cached.fetchedAt < ttlMs) {
|
|
161
|
+
return cached.data;
|
|
162
|
+
}
|
|
163
|
+
const data = await conn.groupMetadata(jid);
|
|
164
|
+
groupMetaCache.set(jid, { data, fetchedAt: now });
|
|
165
|
+
return data;
|
|
166
|
+
},
|
|
167
|
+
/** drops a single group (or the whole cache if no jid given) from getCachedGroupMetadata's cache */
|
|
168
|
+
invalidateGroupMetadataCache(jid) {
|
|
169
|
+
if (jid) {
|
|
170
|
+
groupMetaCache.delete(jid);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
groupMetaCache.clear();
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
/**
|
|
177
|
+
* Scans message text for @628xxx-style mentions and returns the jids
|
|
178
|
+
* it found, so you don't have to write the regex yourself every time
|
|
179
|
+
* you want to build a mentions-enabled message.
|
|
180
|
+
* const mentions = conn.parseMentions('hai @6281234567890 apa kabar')
|
|
181
|
+
* conn.sendMessage(jid, { text, mentions })
|
|
182
|
+
*/
|
|
183
|
+
parseMentions(text) {
|
|
184
|
+
if (!text) {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
const matches = text.match(/@(\d{5,16})/g) || [];
|
|
188
|
+
return matches.map((m) => `${m.slice(1)}@s.whatsapp.net`);
|
|
189
|
+
},
|
|
190
|
+
/**
|
|
191
|
+
* Normalizes a loosely-formatted phone number into a proper WA jid.
|
|
192
|
+
* Strips spaces/dashes/plus/parens, and turns a leading "0" into "62"
|
|
193
|
+
* (change defaultCountryCode if most of your users aren't Indonesian).
|
|
194
|
+
* conn.formatJid('0812-3456-7890') -> '6281234567890@s.whatsapp.net'
|
|
195
|
+
* conn.formatJid('+62 812 3456 7890') -> '6281234567890@s.whatsapp.net'
|
|
196
|
+
*/
|
|
197
|
+
formatJid(numberOrJid, defaultCountryCode = '62') {
|
|
198
|
+
if (!numberOrJid) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (numberOrJid.includes('@')) {
|
|
202
|
+
return numberOrJid;
|
|
203
|
+
}
|
|
204
|
+
let digits = numberOrJid.replace(/[^\d]/g, '');
|
|
205
|
+
if (digits.startsWith('0')) {
|
|
206
|
+
digits = defaultCountryCode + digits.slice(1);
|
|
207
|
+
}
|
|
208
|
+
return `${digits}@s.whatsapp.net`;
|
|
209
|
+
},
|
|
210
|
+
/**
|
|
211
|
+
* Unwraps a view-once message (any version) and returns the real
|
|
212
|
+
* underlying content (imageMessage/videoMessage/audioMessage), so you
|
|
213
|
+
* can download/save it before it's gone. Returns null if the message
|
|
214
|
+
* isn't a view-once wrapper.
|
|
215
|
+
*/
|
|
216
|
+
extractViewOnce(message) {
|
|
217
|
+
if (!message) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const wrapped = message.viewOnceMessage?.message
|
|
221
|
+
|| message.viewOnceMessageV2?.message
|
|
222
|
+
|| message.viewOnceMessageV2Extension?.message
|
|
223
|
+
|| null;
|
|
224
|
+
if (!wrapped) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const innerType = Object.keys(wrapped)[0];
|
|
228
|
+
return { type: innerType, content: wrapped[innerType], message: wrapped };
|
|
229
|
+
},
|
|
230
|
+
/**
|
|
231
|
+
* Downloads whatever media is in a message (image/video/audio/sticker/
|
|
232
|
+
* document), automatically unwrapping view-once first if needed.
|
|
233
|
+
* Returns { buffer, type } or null if there's no media to download.
|
|
234
|
+
*/
|
|
235
|
+
async downloadAnyMedia(message) {
|
|
236
|
+
let target = message;
|
|
237
|
+
const unwrapped = (message?.viewOnceMessage?.message)
|
|
238
|
+
|| (message?.viewOnceMessageV2?.message)
|
|
239
|
+
|| (message?.viewOnceMessageV2Extension?.message);
|
|
240
|
+
if (unwrapped) {
|
|
241
|
+
target = unwrapped;
|
|
242
|
+
}
|
|
243
|
+
const mediaTypes = ['imageMessage', 'videoMessage', 'audioMessage', 'stickerMessage', 'documentMessage', 'documentWithCaptionMessage'];
|
|
244
|
+
const foundType = mediaTypes.find((t) => target?.[t]);
|
|
245
|
+
if (!foundType) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const { downloadMediaMessage } = require('./messages');
|
|
249
|
+
const buffer = await downloadMediaMessage({ message: target }, 'buffer', {});
|
|
250
|
+
return { buffer, type: foundType };
|
|
251
|
+
},
|
|
252
|
+
/**
|
|
253
|
+
* Starts auto-tracking votes for a poll you just sent, so you don't have
|
|
254
|
+
* to manually call getAggregateVotesInPollMessage yourself every time.
|
|
255
|
+
* `pollMsg` is the message object returned by sendMessage() for a poll.
|
|
256
|
+
* Returns a live snapshot getter; call .stop() to stop tracking it.
|
|
257
|
+
*/
|
|
258
|
+
trackPoll(pollMsg) {
|
|
259
|
+
var _a, _b, _c;
|
|
260
|
+
const pollMsgId = pollMsg?.key?.id;
|
|
261
|
+
if (!pollMsgId) {
|
|
262
|
+
throw new Error('trackPoll: pollMsg.key.id is missing');
|
|
263
|
+
}
|
|
264
|
+
const pollCreation = (_c = (_b = (_a = pollMsg.message) === null || _a === void 0 ? void 0 : _a.pollCreationMessage) !== null && _b !== void 0 ? _b : pollMsg.message?.pollCreationMessageV3) !== null && _c !== void 0 ? _c : pollMsg.message?.pollCreationMessageV2;
|
|
265
|
+
const options = (pollCreation?.options || []).map((o) => o.optionName);
|
|
266
|
+
const state = { question: pollCreation?.name || '', options, voters: new Map() };
|
|
267
|
+
pollTallies.set(pollMsgId, state);
|
|
268
|
+
const onUpdate = (updates) => {
|
|
269
|
+
for (const { key, update } of updates) {
|
|
270
|
+
const pollUpdates = update?.pollUpdates;
|
|
271
|
+
if (!pollUpdates || key?.id !== pollMsgId) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const { getAggregateVotesInPollMessage } = require('./messages');
|
|
276
|
+
const tally = getAggregateVotesInPollMessage({ message: pollMsg.message, pollUpdates }, conn.authState?.creds?.me?.id);
|
|
277
|
+
state.lastTally = tally;
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
logger.error({ err }, 'trackPoll: failed to aggregate votes');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
conn.ev.on('messages.update', onUpdate);
|
|
285
|
+
return {
|
|
286
|
+
getResults: () => state.lastTally || options.map((name) => ({ name, voters: [] })),
|
|
287
|
+
stop: () => {
|
|
288
|
+
conn.ev.off('messages.update', onUpdate);
|
|
289
|
+
pollTallies.delete(pollMsgId);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
/**
|
|
294
|
+
* One-stop JID inspector: decodes a jid and tells you exactly what
|
|
295
|
+
* kind of address it is, without having to juggle isJidGroup/isLidUser/
|
|
296
|
+
* isJidUser/jidDecode yourself every time.
|
|
297
|
+
*/
|
|
21
298
|
resolveJid(jid) {
|
|
22
299
|
var _a;
|
|
23
300
|
const { isJidUser, isLidUser, isJidGroup, isJidBroadcast, isJidStatusBroadcast, isJidNewsLetter, isHostedPnUser, isHostedLidUser, jidDecode: decode } = require('../WABinary');
|
|
@@ -57,6 +334,11 @@ const makeBotToolkit = (conn, logger) => {
|
|
|
57
334
|
isPn: kind === 'pn' || kind === 'hosted-pn'
|
|
58
335
|
};
|
|
59
336
|
},
|
|
337
|
+
/**
|
|
338
|
+
* Returns a one-shot snapshot of the connection's health - useful for a
|
|
339
|
+
* `.status` style command without having to manually gather state from
|
|
340
|
+
* five different places.
|
|
341
|
+
*/
|
|
60
342
|
healthCheck() {
|
|
61
343
|
var _a, _b, _c, _d, _e, _f;
|
|
62
344
|
const wsState = (_b = (_a = conn.ws) === null || _a === void 0 ? void 0 : _a.socket) === null || _b === void 0 ? void 0 : _b.readyState;
|
|
@@ -71,6 +353,19 @@ const makeBotToolkit = (conn, logger) => {
|
|
|
71
353
|
rateLimitBucketsTracked: rateLimitBuckets.size
|
|
72
354
|
};
|
|
73
355
|
},
|
|
356
|
+
/**
|
|
357
|
+
* Like `conn.ev.on`, but the handler is isolated: a throw or rejection
|
|
358
|
+
* is caught & logged instead of bubbling up, and an optional timeout
|
|
359
|
+
* guards against a handler that hangs forever (e.g. a stuck network
|
|
360
|
+
* call inside a plugin) from quietly blocking that listener's "lane".
|
|
361
|
+
*
|
|
362
|
+
* @param event event name, e.g. 'messages.upsert'
|
|
363
|
+
* @param handler (data) => any | Promise<any>
|
|
364
|
+
* @param opts.timeoutMs if set, logs a warning if the handler doesn't
|
|
365
|
+
* settle within this time (does NOT kill it -
|
|
366
|
+
* JS can't cancel a running sync/async function -
|
|
367
|
+
* it's a "hey this looks stuck" signal only)
|
|
368
|
+
*/
|
|
74
369
|
onSafe(event, handler, opts = {}) {
|
|
75
370
|
const { timeoutMs } = opts;
|
|
76
371
|
const wrapped = (data) => {
|
|
@@ -101,6 +396,12 @@ const makeBotToolkit = (conn, logger) => {
|
|
|
101
396
|
conn.ev.on(event, wrapped);
|
|
102
397
|
return () => conn.ev.off(event, wrapped);
|
|
103
398
|
},
|
|
399
|
+
/**
|
|
400
|
+
* Returns true if this message id has already been seen recently
|
|
401
|
+
* (within DEDUP_TTL_MS). Marks it as seen either way. Use at the top
|
|
402
|
+
* of your messages.upsert handler to skip WA's occasional duplicate
|
|
403
|
+
* delivery (reconnect races etc.) without writing your own cache.
|
|
404
|
+
*/
|
|
104
405
|
isDuplicateMessage(messageId) {
|
|
105
406
|
if (!messageId) {
|
|
106
407
|
return false;
|
|
@@ -113,6 +414,12 @@ const makeBotToolkit = (conn, logger) => {
|
|
|
113
414
|
}
|
|
114
415
|
return seen;
|
|
115
416
|
},
|
|
417
|
+
/**
|
|
418
|
+
* Simple per-(jid, key) cooldown helper. Returns true if the action
|
|
419
|
+
* is currently rate-limited (i.e. you should NOT proceed), false if
|
|
420
|
+
* it's OK to go ahead (and marks the timestamp).
|
|
421
|
+
* if (conn.isRateLimited(m.chat, 'menu', 5000)) return;
|
|
422
|
+
*/
|
|
116
423
|
isRateLimited(jid, key, windowMs) {
|
|
117
424
|
const bucketKey = `${jid}:${key}`;
|
|
118
425
|
const now = Date.now();
|
|
@@ -122,7 +429,8 @@ const makeBotToolkit = (conn, logger) => {
|
|
|
122
429
|
}
|
|
123
430
|
rateLimitBuckets.set(bucketKey, now);
|
|
124
431
|
return false;
|
|
125
|
-
}
|
|
432
|
+
}
|
|
433
|
+
|
|
126
434
|
async aiMahiru({ errorText, code, apiKey, model = 'claude-haiku-4-5-20251001' }) {
|
|
127
435
|
const key = apiKey || process.env.ANTHROPIC_API_KEY;
|
|
128
436
|
if (!key) {
|
|
@@ -233,9 +233,45 @@ const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
catch (err) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
// self-heal: if decrypt failed using the PN/LID jid we picked,
|
|
237
|
+
// and the other identity (LID<->PN) has a session, try that
|
|
238
|
+
// before giving up - this recovers automatically from the
|
|
239
|
+
// "picked the wrong identity" case without waiting for a
|
|
240
|
+
// retry-receipt round-trip.
|
|
241
|
+
let healed = false;
|
|
242
|
+
if ((attrs.type === 'pkmsg' || attrs.type === 'msg') && (repository === null || repository === void 0 ? void 0 : repository.lidMapping)) {
|
|
243
|
+
try {
|
|
244
|
+
const altJid = (0, WABinary_1.isLidUser)(decryptionJid)
|
|
245
|
+
? await repository.lidMapping.getPNForLID(decryptionJid)
|
|
246
|
+
: await repository.lidMapping.getLIDForPN(decryptionJid);
|
|
247
|
+
if (altJid && altJid !== decryptionJid) {
|
|
248
|
+
logger.debug({ from: decryptionJid, altJid }, 'decrypt failed, retrying with alternate PN/LID identity');
|
|
249
|
+
const retryBuffer = await repository.decryptMessage({
|
|
250
|
+
jid: altJid,
|
|
251
|
+
type: attrs.type,
|
|
252
|
+
ciphertext: content
|
|
253
|
+
});
|
|
254
|
+
let msg = WAProto_1.proto.Message.decode((0, generics_1.unpadRandomMax16)(retryBuffer));
|
|
255
|
+
msg = ((_a = msg.deviceSentMessage) === null || _a === void 0 ? void 0 : _a.message) || msg;
|
|
256
|
+
await processSenderKeyDistribution(msg);
|
|
257
|
+
if (fullMessage.message) {
|
|
258
|
+
Object.assign(fullMessage.message, msg);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
fullMessage.message = msg;
|
|
262
|
+
}
|
|
263
|
+
healed = true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (healErr) {
|
|
267
|
+
logger.debug({ healErr }, 'self-heal retry with alternate identity also failed');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (!healed) {
|
|
271
|
+
logger.error({ key: fullMessage.key, err }, 'failed to decrypt message');
|
|
272
|
+
fullMessage.messageStubType = WAProto_1.proto.WebMessageInfo.StubType.CIPHERTEXT;
|
|
273
|
+
fullMessage.messageStubParameters = [err.message];
|
|
274
|
+
}
|
|
239
275
|
}
|
|
240
276
|
}
|
|
241
277
|
}
|
package/lib/Utils/generics.js
CHANGED
|
@@ -32,6 +32,14 @@ exports.Browsers = (browser) => {
|
|
|
32
32
|
const osRelease = os_1.release();
|
|
33
33
|
return [osName, browser, osRelease];
|
|
34
34
|
};
|
|
35
|
+
// explicit named variants, e.g. Browsers.windows('Chrome') instead of relying
|
|
36
|
+
// on auto-detecting the host OS - useful when you want a stable, specific
|
|
37
|
+
// browser signature regardless of what the bot is actually running on.
|
|
38
|
+
exports.Browsers.ubuntu = (browser) => ['Ubuntu', browser, '22.04.4'];
|
|
39
|
+
exports.Browsers.macOS = (browser) => ['Mac OS', browser, '14.4.1'];
|
|
40
|
+
exports.Browsers.windows = (browser) => ['Windows', browser, '10.0.22631'];
|
|
41
|
+
exports.Browsers.baileys = (browser) => ['Baileys', browser, '6.5.0'];
|
|
42
|
+
exports.Browsers.appropriate = exports.Browsers;
|
|
35
43
|
|
|
36
44
|
const getPlatformId = (browser) => {
|
|
37
45
|
const platformType = WAProto_1.proto.DeviceProps.PlatformType[browser.toUpperCase()];
|
package/lib/Utils/messages.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.assertMediaContent = exports.downloadMediaMessage = exports.aggregateMessageKeysNotFromMe = exports.getAggregateVotesInPollMessage = exports.updateMessageWithPollUpdate = exports.updateMessageWithReaction = exports.updateMessageWithReceipt = exports.getDevice = exports.extractMessageContent = exports.normalizeMessageContent = exports.getContentType = exports.generateWAMessage = exports.generateWAMessageFromContent = exports.generateWAMessageContent = exports.generateForwardMessageContent = exports.prepareDisappearingMessageSettingContent = exports.prepareWAMessageMedia = exports.generateLinkPreviewIfRequired = exports.extractUrlFromText =
|
|
6
|
+
exports.assertMediaContent = exports.downloadMediaMessage = exports.aggregateMessageKeysNotFromMe = exports.getAggregateVotesInPollMessage = exports.updateMessageWithPollUpdate = exports.updateMessageWithReaction = exports.updateMessageWithReceipt = exports.getDevice = exports.extractMessageContent = exports.normalizeMessageContent = exports.getContentType = exports.generateWAMessage = exports.generateWAMessageFromContent = exports.generateWAMessageContent = exports.generateForwardMessageContent = exports.prepareDisappearingMessageSettingContent = exports.prepareWAMessageMedia = exports.generateLinkPreviewIfRequired = exports.extractUrlFromText = void 0;
|
|
7
7
|
const boom_1 = require("@hapi/boom");
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
9
|
const crypto_1 = require("crypto");
|
|
@@ -219,39 +219,6 @@ const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => {
|
|
|
219
219
|
return Types_1.WAProto.Message.fromObject(content);
|
|
220
220
|
};
|
|
221
221
|
exports.prepareDisappearingMessageSettingContent = prepareDisappearingMessageSettingContent;
|
|
222
|
-
/**
|
|
223
|
-
* Wrapper helper to build a `richMessage` content object.
|
|
224
|
-
* Accepts either the flat shape directly:
|
|
225
|
-
* prepareRichResponseMessage({ text: '...', code: { language: 'js', code: '...' } })
|
|
226
|
-
* or a `richResponse` array of fragments (merged automatically: text fragments
|
|
227
|
-
* are joined with a blank line, other keys keep the first value seen):
|
|
228
|
-
* prepareRichResponseMessage({ richResponse: [{ text: '...' }, { code: {...} }] })
|
|
229
|
-
*/
|
|
230
|
-
const prepareRichResponseMessage = (input) => {
|
|
231
|
-
if (input && Array.isArray(input.richResponse)) {
|
|
232
|
-
const merged = {};
|
|
233
|
-
const texts = [];
|
|
234
|
-
for (const item of input.richResponse) {
|
|
235
|
-
if (!item || typeof item !== 'object') {
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
for (const k of Object.keys(item)) {
|
|
239
|
-
if (k === 'text') {
|
|
240
|
-
texts.push(item.text);
|
|
241
|
-
}
|
|
242
|
-
else if (merged[k] === undefined) {
|
|
243
|
-
merged[k] = item[k];
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
if (texts.length) {
|
|
248
|
-
merged.text = texts.join('\n\n');
|
|
249
|
-
}
|
|
250
|
-
return { richMessage: merged };
|
|
251
|
-
}
|
|
252
|
-
return { richMessage: input };
|
|
253
|
-
};
|
|
254
|
-
exports.prepareRichResponseMessage = prepareRichResponseMessage;
|
|
255
222
|
/**
|
|
256
223
|
* Generate forwarded message content like WA does
|
|
257
224
|
* @param message the message to forward
|
|
@@ -413,338 +380,6 @@ const generateWAMessageContent = async (message, options) => {
|
|
|
413
380
|
else if ('requestPhoneNumber' in message) {
|
|
414
381
|
m.requestPhoneNumberMessage = {};
|
|
415
382
|
}
|
|
416
|
-
else if ('richMessage' in message) {
|
|
417
|
-
const { randomUUID } = require('crypto');
|
|
418
|
-
const rich = message.richMessage;
|
|
419
|
-
const submessages = [];
|
|
420
|
-
const sections = [];
|
|
421
|
-
const richResponseSources = [];
|
|
422
|
-
|
|
423
|
-
const extractIE = (text) => {
|
|
424
|
-
let ie = [], result = '', last = 0, citation_index = 1, hyperlink_index = 0, latex_index = 0, stack = [];
|
|
425
|
-
for (let i = 0; i < text.length; i++) {
|
|
426
|
-
if (text[i] == '[' && text[i - 1] != '\\') {
|
|
427
|
-
stack.push(i);
|
|
428
|
-
} else if (text[i] == ']' && (text[i + 1] == '(' || text[i + 1] == '<')) {
|
|
429
|
-
let start = stack.pop();
|
|
430
|
-
if (start == null) continue;
|
|
431
|
-
let open = text[i + 1], close = open == '(' ? ')' : '>', type = open == '(' ? 'link' : 'latex', end = i + 2, depth = 1;
|
|
432
|
-
while (end < text.length && depth) {
|
|
433
|
-
if (text[end] == open && text[end - 1] != '\\') depth++;
|
|
434
|
-
else if (text[end] == close && text[end - 1] != '\\') depth--;
|
|
435
|
-
end++;
|
|
436
|
-
}
|
|
437
|
-
if (depth) continue;
|
|
438
|
-
let raw = text.slice(start + 1, i).trim(), url = text.slice(i + 2, end - 1).trim(), key, tag, data;
|
|
439
|
-
if (type == 'latex') {
|
|
440
|
-
let [txt = '', width = null, height = null, font_height = null, padding = null] = raw.split('|');
|
|
441
|
-
key = `LATEX_${latex_index++}`;
|
|
442
|
-
tag = `{{${key}}}${txt || 'image'}{{/${key}}}`;
|
|
443
|
-
data = { type: 'latex', ie: { key, text: txt, url, width, height, font_height, padding } };
|
|
444
|
-
} else if (raw) {
|
|
445
|
-
key = `HLINK_${hyperlink_index++}`;
|
|
446
|
-
tag = `{{${key}}}${url}{{/${key}}}`;
|
|
447
|
-
data = { type: 'hyperlink', ie: { key, text: raw, url } };
|
|
448
|
-
} else {
|
|
449
|
-
key = `CITE_${citation_index - 1}`;
|
|
450
|
-
tag = `{{${key}}}${url}{{/${key}}}`;
|
|
451
|
-
data = { type: 'citation', ie: { reference_id: citation_index++, key, text: '', url } };
|
|
452
|
-
}
|
|
453
|
-
result += text.slice(last, start) + tag;
|
|
454
|
-
last = end;
|
|
455
|
-
ie.push(data);
|
|
456
|
-
i = end - 1;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
result += text.slice(last);
|
|
460
|
-
return { text: result, ie };
|
|
461
|
-
};
|
|
462
|
-
|
|
463
|
-
const tokenizer = (code, lang = 'javascript') => {
|
|
464
|
-
const keywordsMap = {
|
|
465
|
-
javascript: new Set(['break','case','catch','continue','debugger','delete','do','else','finally','for','function','if','in','instanceof','new','return','switch','this','throw','try','typeof','var','void','while','with','true','false','null','undefined','class','const','let','super','extends','export','import','yield','static','constructor','async','await','get','set'])
|
|
466
|
-
};
|
|
467
|
-
const TYPE_MAP = { 0:'DEFAULT', 1:'KEYWORD', 2:'METHOD', 3:'STR', 4:'NUMBER', 5:'COMMENT' };
|
|
468
|
-
const keywords = keywordsMap[lang] || new Set();
|
|
469
|
-
const tokens = [];
|
|
470
|
-
let i = 0;
|
|
471
|
-
const push = (content, type) => {
|
|
472
|
-
if (!content) return;
|
|
473
|
-
const last = tokens[tokens.length - 1];
|
|
474
|
-
if (last && last.highlightType === type) last.codeContent += content;
|
|
475
|
-
else tokens.push({ codeContent: content, highlightType: type });
|
|
476
|
-
};
|
|
477
|
-
while (i < code.length) {
|
|
478
|
-
const c = code[i];
|
|
479
|
-
if (/\s/.test(c)) { let s = i; while (i < code.length && /\s/.test(code[i])) i++; push(code.slice(s, i), 0); continue; }
|
|
480
|
-
if (c === '/' && code[i + 1] === '/') { let s = i; i += 2; while (i < code.length && code[i] !== '\n') i++; push(code.slice(s, i), 5); continue; }
|
|
481
|
-
if (c === '"' || c === "'" || c === '`') { let s = i; const q = c; i++; while (i < code.length) { if (code[i] === '\\' && i + 1 < code.length) i += 2; else if (code[i] === q) { i++; break; } else i++; } push(code.slice(s, i), 3); continue; }
|
|
482
|
-
if (/[0-9]/.test(c)) { let s = i; while (i < code.length && /[0-9.]/.test(code[i])) i++; push(code.slice(s, i), 4); continue; }
|
|
483
|
-
if (/[a-zA-Z_$]/.test(c)) { let s = i; while (i < code.length && /[a-zA-Z0-9_$]/.test(code[i])) i++; const word = code.slice(s, i); let type = 0; if (keywords.has(word)) type = 1; else { let j = i; while (j < code.length && /\s/.test(code[j])) j++; if (code[j] === '(') type = 2; } push(word, type); continue; }
|
|
484
|
-
push(c, 0); i++;
|
|
485
|
-
}
|
|
486
|
-
return { codeBlock: tokens, unified_codeBlock: tokens.map(t => ({ content: t.codeContent, type: TYPE_MAP[t.highlightType] })) };
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
const toTableMetadata = (arr) => {
|
|
490
|
-
const [header, ...rows] = arr;
|
|
491
|
-
const maxLen = Math.max(header.length, ...rows.map(r => r.length));
|
|
492
|
-
const normalize = (r) => [...r, ...Array(maxLen - r.length).fill('')];
|
|
493
|
-
const unified_rows = [{ is_header: true, cells: normalize(header) }, ...rows.map(r => ({ is_header: false, cells: normalize(r) }))];
|
|
494
|
-
const rowsMeta = unified_rows.map(r => ({ items: r.cells, ...(r.is_header ? { isHeading: true } : {}) }));
|
|
495
|
-
return { title: '', rows: rowsMeta, unified_rows };
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
if (rich.text) {
|
|
499
|
-
const parsed = typeof rich.text === 'string' ? extractIE(rich.text) : rich.text;
|
|
500
|
-
const text = parsed.text || parsed;
|
|
501
|
-
const inline_entities = parsed.ie ? parsed.ie.map(({ type, ie }) => {
|
|
502
|
-
if (type === 'hyperlink') return { key: ie.key, metadata: { display_name: ie.text, is_trusted: true, url: ie.url, __typename: 'GenAIInlineLinkItem' } };
|
|
503
|
-
if (type === 'citation') return { key: ie.key, metadata: { reference_id: ie.reference_id, reference_url: ie.url, reference_title: ie.url, reference_display_name: ie.url, sources: [], __typename: 'GenAISearchCitationItem' } };
|
|
504
|
-
if (type === 'latex') return { key: ie.key, metadata: { latex_expression: ie.text || '', latex_image: { url: ie.url, width: Number(ie.width) || 100, height: Number(ie.height) || 100 }, font_height: Number(ie.font_height) || 83.33, padding: Number(ie.padding) || 15, __typename: 'GenAILatexItem' } };
|
|
505
|
-
return null;
|
|
506
|
-
}).filter(Boolean) : [];
|
|
507
|
-
submessages.push({ messageType: 2, messageText: text });
|
|
508
|
-
sections.push({
|
|
509
|
-
view_model: {
|
|
510
|
-
primitive: { text, inline_entities, __typename: 'GenAIMarkdownTextUXPrimitive' },
|
|
511
|
-
__typename: 'GenAISingleLayoutViewModel'
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (rich.code) {
|
|
517
|
-
const { language, code } = rich.code;
|
|
518
|
-
const tok = tokenizer(code, language);
|
|
519
|
-
submessages.push({ messageType: 5, codeMetadata: { codeLanguage: language, codeBlocks: tok.codeBlock } });
|
|
520
|
-
sections.push({
|
|
521
|
-
view_model: {
|
|
522
|
-
primitive: { language, code_blocks: tok.unified_codeBlock, __typename: 'GenAICodeUXPrimitive' },
|
|
523
|
-
__typename: 'GenAISingleLayoutViewModel'
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (rich.table) {
|
|
529
|
-
const meta = toTableMetadata(rich.table);
|
|
530
|
-
submessages.push({ messageType: 4, tableMetadata: { title: meta.title, rows: meta.rows } });
|
|
531
|
-
sections.push({
|
|
532
|
-
view_model: {
|
|
533
|
-
primitive: { rows: meta.unified_rows, __typename: 'GenATableUXPrimitive' },
|
|
534
|
-
__typename: 'GenAISingleLayoutViewModel'
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (rich.images) {
|
|
540
|
-
const urls = Array.isArray(rich.images) ? rich.images : [rich.images];
|
|
541
|
-
submessages.push({
|
|
542
|
-
messageType: 1,
|
|
543
|
-
gridImageMetadata: {
|
|
544
|
-
gridImageUrl: { imagePreviewUrl: urls[0] },
|
|
545
|
-
imageUrls: urls.map(url => ({ imagePreviewUrl: url, imageHighResUrl: url, sourceUrl: 'https://www.levvicode.cloud/' }))
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
urls.forEach(url => {
|
|
549
|
-
sections.push({
|
|
550
|
-
view_model: {
|
|
551
|
-
primitive: { media: { url, mime_type: 'image/jpeg' }, imagine_type: 3, status: { status: 'READY' }, __typename: 'GenAIImaginePrimitive' },
|
|
552
|
-
__typename: 'GenAISingleLayoutViewModel'
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
if (rich.video) {
|
|
559
|
-
submessages.push({ messageType: 2, messageText: '[ CANNOT_LOAD_VIDEO ]' });
|
|
560
|
-
sections.push({
|
|
561
|
-
view_model: {
|
|
562
|
-
primitive: {
|
|
563
|
-
media: { url: rich.video, mime_type: 'video/mp4', duration: 10 },
|
|
564
|
-
imagine_type: 'ANIMATE',
|
|
565
|
-
status: { status: 'READY' },
|
|
566
|
-
__typename: 'GenAIImaginePrimitive'
|
|
567
|
-
},
|
|
568
|
-
__typename: 'GenAISingleLayoutViewModel'
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (rich.productSingle) {
|
|
574
|
-
submessages.push({ messageType: 2, messageText: '[ CANNOT_LOAD_PRODUCT ]' });
|
|
575
|
-
sections.push({
|
|
576
|
-
view_model: {
|
|
577
|
-
primitive: { ...rich.productSingle, __typename: 'GenAIProductItemCardPrimitive' },
|
|
578
|
-
__typename: 'GenAISingleLayoutViewModel'
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
if (rich.productMultiple) {
|
|
584
|
-
submessages.push({ messageType: 2, messageText: '[ CANNOT_LOAD_PRODUCT ]' });
|
|
585
|
-
sections.push({
|
|
586
|
-
view_model: {
|
|
587
|
-
primitives: rich.productMultiple.map(p => ({ ...p, __typename: 'GenAIProductItemCardPrimitive' })),
|
|
588
|
-
__typename: 'GenAIHScrollLayoutViewModel'
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
if (rich.product && !rich.productSingle && !rich.productMultiple) {
|
|
594
|
-
submessages.push({ messageType: 2, messageText: '[ CANNOT_LOAD_PRODUCT ]' });
|
|
595
|
-
if (Array.isArray(rich.product)) {
|
|
596
|
-
sections.push({
|
|
597
|
-
view_model: {
|
|
598
|
-
primitives: rich.product.map(p => ({ ...p, __typename: 'GenAIProductItemCardPrimitive' })),
|
|
599
|
-
__typename: 'GenAIHScrollLayoutViewModel'
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
} else {
|
|
603
|
-
sections.push({
|
|
604
|
-
view_model: {
|
|
605
|
-
primitive: { ...rich.product, __typename: 'GenAIProductItemCardPrimitive' },
|
|
606
|
-
__typename: 'GenAISingleLayoutViewModel'
|
|
607
|
-
}
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (rich.post) {
|
|
613
|
-
submessages.push({ messageType: 2, messageText: '[ CANNOT_LOAD_POST ]' });
|
|
614
|
-
if (Array.isArray(rich.post)) {
|
|
615
|
-
sections.push({
|
|
616
|
-
view_model: {
|
|
617
|
-
primitives: rich.post.map(p => ({ ...p, __typename: 'GenAIPostPrimitive' })),
|
|
618
|
-
__typename: 'GenAIHScrollLayoutViewModel'
|
|
619
|
-
}
|
|
620
|
-
});
|
|
621
|
-
} else {
|
|
622
|
-
sections.push({
|
|
623
|
-
view_model: {
|
|
624
|
-
primitive: { ...rich.post, __typename: 'GenAIPostPrimitive' },
|
|
625
|
-
__typename: 'GenAISingleLayoutViewModel'
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
if (rich.reels) {
|
|
632
|
-
const items = Array.isArray(rich.reels) ? rich.reels : [rich.reels];
|
|
633
|
-
submessages.push({
|
|
634
|
-
messageType: 9,
|
|
635
|
-
contentItemsMetadata: {
|
|
636
|
-
contentType: 1,
|
|
637
|
-
itemsMetadata: items.map(i => ({
|
|
638
|
-
reelItem: { title: i.title, profileIconUrl: i.profileIconUrl, thumbnailUrl: i.thumbnailUrl, videoUrl: i.videoUrl }
|
|
639
|
-
}))
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
sections.push({
|
|
643
|
-
view_model: {
|
|
644
|
-
primitives: items.map(i => ({
|
|
645
|
-
reels_url: i.videoUrl,
|
|
646
|
-
thumbnail_url: i.thumbnailUrl,
|
|
647
|
-
creator: i.title,
|
|
648
|
-
avatar_url: i.profileIconUrl,
|
|
649
|
-
reels_title: i.reels_title || '',
|
|
650
|
-
likes_count: i.likes_count || 0,
|
|
651
|
-
shares_count: i.shares_count || 0,
|
|
652
|
-
view_count: i.view_count || 0,
|
|
653
|
-
reel_source: i.reel_source || 'IG',
|
|
654
|
-
is_verified: i.is_verified || false,
|
|
655
|
-
__typename: 'GenAIReelPrimitive'
|
|
656
|
-
})),
|
|
657
|
-
__typename: 'GenAIHScrollLayoutViewModel'
|
|
658
|
-
}
|
|
659
|
-
});
|
|
660
|
-
items.forEach((i, idx) => richResponseSources.push({
|
|
661
|
-
provider: 'MahiruBaileys',
|
|
662
|
-
thumbnailCDNURL: i.thumbnailUrl,
|
|
663
|
-
sourceProviderURL: i.videoUrl,
|
|
664
|
-
sourceQuery: '',
|
|
665
|
-
faviconCDNURL: i.profileIconUrl,
|
|
666
|
-
citationNumber: idx + 1,
|
|
667
|
-
sourceTitle: i.title
|
|
668
|
-
}));
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
if (rich.sources) {
|
|
672
|
-
const sourceArr = Array.isArray(rich.sources) ? rich.sources : [rich.sources];
|
|
673
|
-
sections.push({
|
|
674
|
-
view_model: {
|
|
675
|
-
primitive: {
|
|
676
|
-
sources: sourceArr.map(s => typeof s === 'object' ? s : {
|
|
677
|
-
source_type: 'THIRD_PARTY',
|
|
678
|
-
source_display_name: s[2] || '',
|
|
679
|
-
source_subtitle: 'AI',
|
|
680
|
-
source_url: s[1] || '',
|
|
681
|
-
favicon: { url: s[0] || '', mime_type: 'image/jpeg', width: 16, height: 16 }
|
|
682
|
-
}),
|
|
683
|
-
__typename: 'GenAISearchResultPrimitive'
|
|
684
|
-
},
|
|
685
|
-
__typename: 'GenAISingleLayoutViewModel'
|
|
686
|
-
}
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
if (rich.tip) {
|
|
691
|
-
submessages.push({ messageType: 2, messageText: rich.tip });
|
|
692
|
-
sections.push({
|
|
693
|
-
view_model: {
|
|
694
|
-
primitive: { text: rich.tip, __typename: 'GenAIMetadataTextPrimitive' },
|
|
695
|
-
__typename: 'GenAISingleLayoutViewModel'
|
|
696
|
-
}
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
if (rich.suggestions) {
|
|
701
|
-
sections.push({
|
|
702
|
-
view_model: {
|
|
703
|
-
primitives: rich.suggestions.map(s => ({
|
|
704
|
-
prompt_text: s,
|
|
705
|
-
prompt_type: 'SUGGESTED_PROMPT',
|
|
706
|
-
__typename: 'GenAIFollowUpSuggestionPillPrimitive'
|
|
707
|
-
})),
|
|
708
|
-
__typename: 'GenAIActionRowLayoutViewModel'
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
if (rich.footer) {
|
|
714
|
-
sections.push({
|
|
715
|
-
view_model: {
|
|
716
|
-
primitive: { text: rich.footer, __typename: 'GenAIMetadataTextPrimitive' },
|
|
717
|
-
__typename: 'GenAISingleLayoutViewModel'
|
|
718
|
-
}
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const unifiedData = { response_id: randomUUID(), sections };
|
|
723
|
-
|
|
724
|
-
m = {
|
|
725
|
-
messageContextInfo: {
|
|
726
|
-
deviceListMetadata: {},
|
|
727
|
-
deviceListMetadataVersion: 2,
|
|
728
|
-
botMetadata: {
|
|
729
|
-
messageDisclaimerText: rich.title || '',
|
|
730
|
-
richResponseSourcesMetadata: { sources: richResponseSources }
|
|
731
|
-
}
|
|
732
|
-
},
|
|
733
|
-
richResponseMessage: {
|
|
734
|
-
messageType: 1,
|
|
735
|
-
submessages,
|
|
736
|
-
unifiedResponse: {
|
|
737
|
-
data: Buffer.from(JSON.stringify(unifiedData)).toString('base64')
|
|
738
|
-
},
|
|
739
|
-
contextInfo: {
|
|
740
|
-
forwardingScore: 1,
|
|
741
|
-
isForwarded: true,
|
|
742
|
-
forwardedAiBotMessageInfo: { botJid: '0@bot' },
|
|
743
|
-
forwardOrigin: 4
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
383
|
else {
|
|
749
384
|
m = await (0, exports.prepareWAMessageMedia)(message, options);
|
|
750
385
|
}
|
|
@@ -29,7 +29,12 @@ const useMultiFileAuthState = async (folder) => {
|
|
|
29
29
|
const mutex = getFileLock(filePath);
|
|
30
30
|
return mutex.acquire().then(async (release) => {
|
|
31
31
|
try {
|
|
32
|
-
|
|
32
|
+
// write to a temp file first, then rename - rename is atomic on
|
|
33
|
+
// the same filesystem, so a crash/kill mid-write can never leave
|
|
34
|
+
// creds.json (or any key file) half-written/corrupted
|
|
35
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
36
|
+
await (0, promises_1.writeFile)(tmpPath, JSON.stringify(data, generics_1.BufferJSON.replacer));
|
|
37
|
+
await (0, promises_1.rename)(tmpPath, filePath);
|
|
33
38
|
}
|
|
34
39
|
finally {
|
|
35
40
|
release();
|