@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.
@@ -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,
@@ -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;
@@ -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 jid = WABinary_1.jidEncode(user, (groupData === null || groupData === void 0 ? void 0 : groupData.addressingMode) === 'lid' ? 'lid' : 's.whatsapp.net', device);
345
- if (!senderKeyMap[jid] || !!participant) {
346
- senderKeyJids.push(jid);
347
- senderKeyMap[jid] = true;
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) {
@@ -690,7 +690,7 @@ const makeSocket = (config) => {
690
690
  if (printQRInTerminal) {
691
691
  (0, Utils_1.printQRIfNecessaryListener)(ev, logger);
692
692
  }
693
- return {
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
  /**
@@ -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
- 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
  }
@@ -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()];
@@ -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 = exports.prepareRichResponseMessage = void 0;
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
- await (0, promises_1.writeFile)(filePath, JSON.stringify(data, generics_1.BufferJSON.replacer));
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langitdeveloper/baileys",
3
- "version": "2.1.9",
3
+ "version": "2.2.1",
4
4
  "description": "WhatsApp API Modification By Langit",
5
5
  "keywords": [
6
6
  "whatsapp",