@langitdeveloper/baileys 2.1.9 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Socket/business.js +51 -1
- package/lib/Socket/chats.js +87 -0
- package/lib/Socket/dugong.js +21 -0
- package/lib/Socket/messages-recv.js +1 -1
- package/lib/Socket/messages-send.js +10 -1
- package/lib/Utils/bot-toolkit.js +250 -1
- package/lib/Utils/decode-wa-message.js +40 -4
- package/lib/Utils/generics.js +9 -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,
|
|
@@ -257,4 +307,4 @@ const makeBusinessSocket = (config) => {
|
|
|
257
307
|
productUpdate
|
|
258
308
|
};
|
|
259
309
|
};
|
|
260
|
-
exports.makeBusinessSocket = makeBusinessSocket;
|
|
310
|
+
exports.makeBusinessSocket = makeBusinessSocket;
|
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,8 +341,16 @@ const makeMessagesSocket = (config) => {
|
|
|
340
341
|
meId,
|
|
341
342
|
});
|
|
342
343
|
const senderKeyJids = [];
|
|
344
|
+
const { user: mePnUser } = WABinary_1.jidDecode(meId);
|
|
345
|
+
const { user: meLidUser } = meLid ? WABinary_1.jidDecode(meLid) : { user: null };
|
|
343
346
|
for (const { user, device } of devices) {
|
|
344
347
|
const jid = WABinary_1.jidEncode(user, (groupData === null || groupData === void 0 ? void 0 : groupData.addressingMode) === 'lid' ? 'lid' : 's.whatsapp.net', device);
|
|
348
|
+
// skip the exact device that sent this message - it already has the content
|
|
349
|
+
const isExactSenderDevice = jid === meId || (meLid && jid === meLid);
|
|
350
|
+
if (isExactSenderDevice) {
|
|
351
|
+
logger.debug({ jid, meId, meLid }, 'Skipping exact sender device (whatsmeow pattern)');
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
345
354
|
if (!senderKeyMap[jid] || !!participant) {
|
|
346
355
|
senderKeyJids.push(jid);
|
|
347
356
|
senderKeyMap[jid] = true;
|
|
@@ -828,4 +837,4 @@ const makeMessagesSocket = (config) => {
|
|
|
828
837
|
}
|
|
829
838
|
}
|
|
830
839
|
};
|
|
831
|
-
exports.makeMessagesSocket = makeMessagesSocket;
|
|
840
|
+
exports.makeMessagesSocket = makeMessagesSocket;
|
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,207 @@ 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
|
+
* Downloads any URL into a Buffer - the one-liner you end up writing
|
|
32
|
+
* in every plugin that needs to grab an image/file from the internet
|
|
33
|
+
* before sending it.
|
|
34
|
+
*/
|
|
35
|
+
async getBuffer(url, opts = {}) {
|
|
36
|
+
const res = await fetch(url, opts);
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
throw new Error(`getBuffer: HTTP ${res.status} fetching ${url}`);
|
|
39
|
+
}
|
|
40
|
+
const arrBuf = await res.arrayBuffer();
|
|
41
|
+
return Buffer.from(arrBuf);
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* Downloads a URL and sends it as the right message type automatically,
|
|
45
|
+
* based on the response's content-type (falls back to sniffing the
|
|
46
|
+
* file extension in the URL if the server doesn't send one).
|
|
47
|
+
* await conn.sendFileFromUrl(jid, 'https://example.com/cat.png', { caption: 'meow' })
|
|
48
|
+
*/
|
|
49
|
+
async sendFileFromUrl(jid, url, options = {}) {
|
|
50
|
+
const res = await fetch(url);
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
throw new Error(`sendFileFromUrl: HTTP ${res.status} fetching ${url}`);
|
|
53
|
+
}
|
|
54
|
+
const contentType = res.headers.get('content-type') || '';
|
|
55
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
56
|
+
let contentKey = 'document';
|
|
57
|
+
if (contentType.startsWith('image/') || /\.(jpe?g|png|webp|gif)$/i.test(url)) {
|
|
58
|
+
contentKey = 'image';
|
|
59
|
+
}
|
|
60
|
+
else if (contentType.startsWith('video/') || /\.(mp4|mkv|mov)$/i.test(url)) {
|
|
61
|
+
contentKey = 'video';
|
|
62
|
+
}
|
|
63
|
+
else if (contentType.startsWith('audio/') || /\.(mp3|ogg|wav|m4a)$/i.test(url)) {
|
|
64
|
+
contentKey = 'audio';
|
|
65
|
+
}
|
|
66
|
+
const content = { [contentKey]: buffer, ...options };
|
|
67
|
+
if (contentKey === 'document' && !content.mimetype) {
|
|
68
|
+
content.mimetype = contentType || 'application/octet-stream';
|
|
69
|
+
}
|
|
70
|
+
return conn.sendMessage(jid, content, options.messageOptions || {});
|
|
71
|
+
},
|
|
72
|
+
/** human-readable uptime string, e.g. "2h 14m 9s", for status/.ping commands */
|
|
73
|
+
uptimeString() {
|
|
74
|
+
const ms = Date.now() - startedAt;
|
|
75
|
+
const s = Math.floor(ms / 1000) % 60;
|
|
76
|
+
const m = Math.floor(ms / 60000) % 60;
|
|
77
|
+
const h = Math.floor(ms / 3600000);
|
|
78
|
+
return `${h}h ${m}m ${s}s`;
|
|
79
|
+
},
|
|
80
|
+
/**
|
|
81
|
+
* Cached groupMetadata - avoids hammering WA's servers when you call
|
|
82
|
+
* groupMetadata() repeatedly for the same group in a short window
|
|
83
|
+
* (e.g. every message handler checking admin status). Falls back to
|
|
84
|
+
* a real fetch automatically once the cache entry goes stale.
|
|
85
|
+
*/
|
|
86
|
+
async getCachedGroupMetadata(jid, ttlMs = GROUP_META_TTL_MS) {
|
|
87
|
+
const cached = groupMetaCache.get(jid);
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
if (cached && now - cached.fetchedAt < ttlMs) {
|
|
90
|
+
return cached.data;
|
|
91
|
+
}
|
|
92
|
+
const data = await conn.groupMetadata(jid);
|
|
93
|
+
groupMetaCache.set(jid, { data, fetchedAt: now });
|
|
94
|
+
return data;
|
|
95
|
+
},
|
|
96
|
+
/** drops a single group (or the whole cache if no jid given) from getCachedGroupMetadata's cache */
|
|
97
|
+
invalidateGroupMetadataCache(jid) {
|
|
98
|
+
if (jid) {
|
|
99
|
+
groupMetaCache.delete(jid);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
groupMetaCache.clear();
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
/**
|
|
106
|
+
* Scans message text for @628xxx-style mentions and returns the jids
|
|
107
|
+
* it found, so you don't have to write the regex yourself every time
|
|
108
|
+
* you want to build a mentions-enabled message.
|
|
109
|
+
* const mentions = conn.parseMentions('hai @6281234567890 apa kabar')
|
|
110
|
+
* conn.sendMessage(jid, { text, mentions })
|
|
111
|
+
*/
|
|
112
|
+
parseMentions(text) {
|
|
113
|
+
if (!text) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const matches = text.match(/@(\d{5,16})/g) || [];
|
|
117
|
+
return matches.map((m) => `${m.slice(1)}@s.whatsapp.net`);
|
|
118
|
+
},
|
|
119
|
+
/**
|
|
120
|
+
* Normalizes a loosely-formatted phone number into a proper WA jid.
|
|
121
|
+
* Strips spaces/dashes/plus/parens, and turns a leading "0" into "62"
|
|
122
|
+
* (change defaultCountryCode if most of your users aren't Indonesian).
|
|
123
|
+
* conn.formatJid('0812-3456-7890') -> '6281234567890@s.whatsapp.net'
|
|
124
|
+
* conn.formatJid('+62 812 3456 7890') -> '6281234567890@s.whatsapp.net'
|
|
125
|
+
*/
|
|
126
|
+
formatJid(numberOrJid, defaultCountryCode = '62') {
|
|
127
|
+
if (!numberOrJid) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
if (numberOrJid.includes('@')) {
|
|
131
|
+
return numberOrJid;
|
|
132
|
+
}
|
|
133
|
+
let digits = numberOrJid.replace(/[^\d]/g, '');
|
|
134
|
+
if (digits.startsWith('0')) {
|
|
135
|
+
digits = defaultCountryCode + digits.slice(1);
|
|
136
|
+
}
|
|
137
|
+
return `${digits}@s.whatsapp.net`;
|
|
138
|
+
},
|
|
139
|
+
/**
|
|
140
|
+
* Unwraps a view-once message (any version) and returns the real
|
|
141
|
+
* underlying content (imageMessage/videoMessage/audioMessage), so you
|
|
142
|
+
* can download/save it before it's gone. Returns null if the message
|
|
143
|
+
* isn't a view-once wrapper.
|
|
144
|
+
*/
|
|
145
|
+
extractViewOnce(message) {
|
|
146
|
+
if (!message) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const wrapped = message.viewOnceMessage?.message
|
|
150
|
+
|| message.viewOnceMessageV2?.message
|
|
151
|
+
|| message.viewOnceMessageV2Extension?.message
|
|
152
|
+
|| null;
|
|
153
|
+
if (!wrapped) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const innerType = Object.keys(wrapped)[0];
|
|
157
|
+
return { type: innerType, content: wrapped[innerType], message: wrapped };
|
|
158
|
+
},
|
|
159
|
+
/**
|
|
160
|
+
* Downloads whatever media is in a message (image/video/audio/sticker/
|
|
161
|
+
* document), automatically unwrapping view-once first if needed.
|
|
162
|
+
* Returns { buffer, type } or null if there's no media to download.
|
|
163
|
+
*/
|
|
164
|
+
async downloadAnyMedia(message) {
|
|
165
|
+
let target = message;
|
|
166
|
+
const unwrapped = (message?.viewOnceMessage?.message)
|
|
167
|
+
|| (message?.viewOnceMessageV2?.message)
|
|
168
|
+
|| (message?.viewOnceMessageV2Extension?.message);
|
|
169
|
+
if (unwrapped) {
|
|
170
|
+
target = unwrapped;
|
|
171
|
+
}
|
|
172
|
+
const mediaTypes = ['imageMessage', 'videoMessage', 'audioMessage', 'stickerMessage', 'documentMessage', 'documentWithCaptionMessage'];
|
|
173
|
+
const foundType = mediaTypes.find((t) => target?.[t]);
|
|
174
|
+
if (!foundType) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const { downloadMediaMessage } = require('./messages');
|
|
178
|
+
const buffer = await downloadMediaMessage({ message: target }, 'buffer', {});
|
|
179
|
+
return { buffer, type: foundType };
|
|
180
|
+
},
|
|
181
|
+
/**
|
|
182
|
+
* Starts auto-tracking votes for a poll you just sent, so you don't have
|
|
183
|
+
* to manually call getAggregateVotesInPollMessage yourself every time.
|
|
184
|
+
* `pollMsg` is the message object returned by sendMessage() for a poll.
|
|
185
|
+
* Returns a live snapshot getter; call .stop() to stop tracking it.
|
|
186
|
+
*/
|
|
187
|
+
trackPoll(pollMsg) {
|
|
188
|
+
var _a, _b, _c;
|
|
189
|
+
const pollMsgId = pollMsg?.key?.id;
|
|
190
|
+
if (!pollMsgId) {
|
|
191
|
+
throw new Error('trackPoll: pollMsg.key.id is missing');
|
|
192
|
+
}
|
|
193
|
+
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;
|
|
194
|
+
const options = (pollCreation?.options || []).map((o) => o.optionName);
|
|
195
|
+
const state = { question: pollCreation?.name || '', options, voters: new Map() };
|
|
196
|
+
pollTallies.set(pollMsgId, state);
|
|
197
|
+
const onUpdate = (updates) => {
|
|
198
|
+
for (const { key, update } of updates) {
|
|
199
|
+
const pollUpdates = update?.pollUpdates;
|
|
200
|
+
if (!pollUpdates || key?.id !== pollMsgId) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const { getAggregateVotesInPollMessage } = require('./messages');
|
|
205
|
+
const tally = getAggregateVotesInPollMessage({ message: pollMsg.message, pollUpdates }, conn.authState?.creds?.me?.id);
|
|
206
|
+
state.lastTally = tally;
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
logger.error({ err }, 'trackPoll: failed to aggregate votes');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
conn.ev.on('messages.update', onUpdate);
|
|
214
|
+
return {
|
|
215
|
+
getResults: () => state.lastTally || options.map((name) => ({ name, voters: [] })),
|
|
216
|
+
stop: () => {
|
|
217
|
+
conn.ev.off('messages.update', onUpdate);
|
|
218
|
+
pollTallies.delete(pollMsgId);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
/**
|
|
223
|
+
* One-stop JID inspector: decodes a jid and tells you exactly what
|
|
224
|
+
* kind of address it is, without having to juggle isJidGroup/isLidUser/
|
|
225
|
+
* isJidUser/jidDecode yourself every time.
|
|
226
|
+
*/
|
|
21
227
|
resolveJid(jid) {
|
|
22
228
|
var _a;
|
|
23
229
|
const { isJidUser, isLidUser, isJidGroup, isJidBroadcast, isJidStatusBroadcast, isJidNewsLetter, isHostedPnUser, isHostedLidUser, jidDecode: decode } = require('../WABinary');
|
|
@@ -57,6 +263,11 @@ const makeBotToolkit = (conn, logger) => {
|
|
|
57
263
|
isPn: kind === 'pn' || kind === 'hosted-pn'
|
|
58
264
|
};
|
|
59
265
|
},
|
|
266
|
+
/**
|
|
267
|
+
* Returns a one-shot snapshot of the connection's health - useful for a
|
|
268
|
+
* `.status` style command without having to manually gather state from
|
|
269
|
+
* five different places.
|
|
270
|
+
*/
|
|
60
271
|
healthCheck() {
|
|
61
272
|
var _a, _b, _c, _d, _e, _f;
|
|
62
273
|
const wsState = (_b = (_a = conn.ws) === null || _a === void 0 ? void 0 : _a.socket) === null || _b === void 0 ? void 0 : _b.readyState;
|
|
@@ -71,6 +282,19 @@ const makeBotToolkit = (conn, logger) => {
|
|
|
71
282
|
rateLimitBucketsTracked: rateLimitBuckets.size
|
|
72
283
|
};
|
|
73
284
|
},
|
|
285
|
+
/**
|
|
286
|
+
* Like `conn.ev.on`, but the handler is isolated: a throw or rejection
|
|
287
|
+
* is caught & logged instead of bubbling up, and an optional timeout
|
|
288
|
+
* guards against a handler that hangs forever (e.g. a stuck network
|
|
289
|
+
* call inside a plugin) from quietly blocking that listener's "lane".
|
|
290
|
+
*
|
|
291
|
+
* @param event event name, e.g. 'messages.upsert'
|
|
292
|
+
* @param handler (data) => any | Promise<any>
|
|
293
|
+
* @param opts.timeoutMs if set, logs a warning if the handler doesn't
|
|
294
|
+
* settle within this time (does NOT kill it -
|
|
295
|
+
* JS can't cancel a running sync/async function -
|
|
296
|
+
* it's a "hey this looks stuck" signal only)
|
|
297
|
+
*/
|
|
74
298
|
onSafe(event, handler, opts = {}) {
|
|
75
299
|
const { timeoutMs } = opts;
|
|
76
300
|
const wrapped = (data) => {
|
|
@@ -101,6 +325,12 @@ const makeBotToolkit = (conn, logger) => {
|
|
|
101
325
|
conn.ev.on(event, wrapped);
|
|
102
326
|
return () => conn.ev.off(event, wrapped);
|
|
103
327
|
},
|
|
328
|
+
/**
|
|
329
|
+
* Returns true if this message id has already been seen recently
|
|
330
|
+
* (within DEDUP_TTL_MS). Marks it as seen either way. Use at the top
|
|
331
|
+
* of your messages.upsert handler to skip WA's occasional duplicate
|
|
332
|
+
* delivery (reconnect races etc.) without writing your own cache.
|
|
333
|
+
*/
|
|
104
334
|
isDuplicateMessage(messageId) {
|
|
105
335
|
if (!messageId) {
|
|
106
336
|
return false;
|
|
@@ -113,6 +343,12 @@ const makeBotToolkit = (conn, logger) => {
|
|
|
113
343
|
}
|
|
114
344
|
return seen;
|
|
115
345
|
},
|
|
346
|
+
/**
|
|
347
|
+
* Simple per-(jid, key) cooldown helper. Returns true if the action
|
|
348
|
+
* is currently rate-limited (i.e. you should NOT proceed), false if
|
|
349
|
+
* it's OK to go ahead (and marks the timestamp).
|
|
350
|
+
* if (conn.isRateLimited(m.chat, 'menu', 5000)) return;
|
|
351
|
+
*/
|
|
116
352
|
isRateLimited(jid, key, windowMs) {
|
|
117
353
|
const bucketKey = `${jid}:${key}`;
|
|
118
354
|
const now = Date.now();
|
|
@@ -123,6 +359,19 @@ const makeBotToolkit = (conn, logger) => {
|
|
|
123
359
|
rateLimitBuckets.set(bucketKey, now);
|
|
124
360
|
return false;
|
|
125
361
|
},
|
|
362
|
+
/**
|
|
363
|
+
* Asks an Anthropic model to help debug an error / snippet against
|
|
364
|
+
* THIS fork's actual Baileys source, so suggestions are grounded in
|
|
365
|
+
* what's really in your codebase instead of generic upstream advice.
|
|
366
|
+
* Wire this up to whatever command prefix you like in your own bot
|
|
367
|
+
* dispatcher (e.g. `.aimahiru`) - this function only does the actual
|
|
368
|
+
* call + prompt shaping, not command parsing.
|
|
369
|
+
*
|
|
370
|
+
* @param input.errorText the error/stack trace the user is hitting
|
|
371
|
+
* @param input.code the snippet of their bot code, if any
|
|
372
|
+
* @param input.apiKey Anthropic API key (or set ANTHROPIC_API_KEY env)
|
|
373
|
+
* @param input.model defaults to a Haiku-class model for speed/cost
|
|
374
|
+
*/
|
|
126
375
|
async aiMahiru({ errorText, code, apiKey, model = 'claude-haiku-4-5-20251001' }) {
|
|
127
376
|
const key = apiKey || process.env.ANTHROPIC_API_KEY;
|
|
128
377
|
if (!key) {
|
|
@@ -162,4 +411,4 @@ const makeBotToolkit = (conn, logger) => {
|
|
|
162
411
|
}
|
|
163
412
|
};
|
|
164
413
|
};
|
|
165
|
-
exports.makeBotToolkit = makeBotToolkit;
|
|
414
|
+
exports.makeBotToolkit = makeBotToolkit;
|
|
@@ -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
|
}
|
|
@@ -247,4 +283,4 @@ const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
|
|
|
247
283
|
}
|
|
248
284
|
};
|
|
249
285
|
};
|
|
250
|
-
exports.decryptMessageNode = decryptMessageNode;
|
|
286
|
+
exports.decryptMessageNode = decryptMessageNode;
|
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()];
|
|
@@ -495,4 +503,4 @@ exports.bytesToCrockford = bytesToCrockford;
|
|
|
495
503
|
const encodeNewsletterMessage = (message) => {
|
|
496
504
|
return WAProto_1.proto.Message.encode(message).finish()
|
|
497
505
|
}
|
|
498
|
-
exports.encodeNewsletterMessage = encodeNewsletterMessage;
|
|
506
|
+
exports.encodeNewsletterMessage = encodeNewsletterMessage;
|