@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.
@@ -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;
@@ -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,
@@ -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;
@@ -1142,4 +1142,4 @@ const makeMessagesRecvSocket = (config) => {
1142
1142
  requestPlaceholderResend,
1143
1143
  };
1144
1144
  };
1145
- exports.makeMessagesRecvSocket = makeMessagesRecvSocket;
1145
+ exports.makeMessagesRecvSocket = makeMessagesRecvSocket;
@@ -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;
@@ -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
- logger.error({ key: fullMessage.key, err }, 'failed to decrypt message');
237
- fullMessage.messageStubType = WAProto_1.proto.WebMessageInfo.StubType.CIPHERTEXT;
238
- fullMessage.messageStubParameters = [err.message];
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;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langitdeveloper/baileys",
3
- "version": "2.1.9",
3
+ "version": "2.2.0",
4
4
  "description": "WhatsApp API Modification By Langit",
5
5
  "keywords": [
6
6
  "whatsapp",