@awangxcode/qiro-baileys 7.0.0-rc22 → 7.0.0-rc25

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.
Files changed (64) hide show
  1. package/README.md +7 -1
  2. package/WAProto/WAProto.proto +47 -0
  3. package/WAProto/index.d.ts +115 -2
  4. package/WAProto/index.js +299 -0
  5. package/lib/Defaults/index.d.ts.map +1 -1
  6. package/lib/Defaults/index.js +2 -0
  7. package/lib/Defaults/index.js.map +1 -1
  8. package/lib/Extensions/advanced.d.ts +247 -0
  9. package/lib/Extensions/advanced.d.ts.map +1 -0
  10. package/lib/Extensions/advanced.js +821 -0
  11. package/lib/Extensions/advanced.js.map +1 -0
  12. package/lib/Extensions/experimental.d.ts +378 -0
  13. package/lib/Extensions/experimental.d.ts.map +1 -0
  14. package/lib/Extensions/experimental.js +628 -0
  15. package/lib/Extensions/experimental.js.map +1 -0
  16. package/lib/MessageBuilder/index.d.ts +4 -0
  17. package/lib/MessageBuilder/index.d.ts.map +1 -1
  18. package/lib/MessageBuilder/index.js +4 -1
  19. package/lib/MessageBuilder/index.js.map +1 -1
  20. package/lib/Socket/business.d.ts +5 -3
  21. package/lib/Socket/business.d.ts.map +1 -1
  22. package/lib/Socket/chats.d.ts +4 -2
  23. package/lib/Socket/chats.d.ts.map +1 -1
  24. package/lib/Socket/chats.js.map +1 -1
  25. package/lib/Socket/communities.d.ts +5 -3
  26. package/lib/Socket/communities.d.ts.map +1 -1
  27. package/lib/Socket/groups.d.ts +4 -2
  28. package/lib/Socket/groups.d.ts.map +1 -1
  29. package/lib/Socket/index.d.ts +6 -4
  30. package/lib/Socket/index.d.ts.map +1 -1
  31. package/lib/Socket/index.js +9 -1
  32. package/lib/Socket/index.js.map +1 -1
  33. package/lib/Socket/messages-recv.d.ts +5 -3
  34. package/lib/Socket/messages-recv.d.ts.map +1 -1
  35. package/lib/Socket/messages-send.d.ts +5 -3
  36. package/lib/Socket/messages-send.d.ts.map +1 -1
  37. package/lib/Socket/messages-send.js +65 -5
  38. package/lib/Socket/messages-send.js.map +1 -1
  39. package/lib/Socket/newsletter.d.ts +4 -2
  40. package/lib/Socket/newsletter.d.ts.map +1 -1
  41. package/lib/Types/Message.d.ts +11 -0
  42. package/lib/Types/Message.d.ts.map +1 -1
  43. package/lib/Types/Socket.d.ts +4 -0
  44. package/lib/Types/Socket.d.ts.map +1 -1
  45. package/lib/Utils/messages-media.d.ts +4 -2
  46. package/lib/Utils/messages-media.d.ts.map +1 -1
  47. package/lib/Utils/messages-media.js +14 -11
  48. package/lib/Utils/messages-media.js.map +1 -1
  49. package/lib/Utils/messages.d.ts.map +1 -1
  50. package/lib/Utils/messages.js +20 -3
  51. package/lib/Utils/messages.js.map +1 -1
  52. package/lib/Utils/terminal-welcome.d.ts.map +1 -1
  53. package/lib/Utils/terminal-welcome.js +13 -2
  54. package/lib/Utils/terminal-welcome.js.map +1 -1
  55. package/lib/WABinary/generic-utils.d.ts +16 -0
  56. package/lib/WABinary/generic-utils.d.ts.map +1 -1
  57. package/lib/WABinary/generic-utils.js +111 -0
  58. package/lib/WABinary/generic-utils.js.map +1 -1
  59. package/lib/index.d.ts +2 -0
  60. package/lib/index.d.ts.map +1 -1
  61. package/lib/index.js +2 -0
  62. package/lib/index.js.map +1 -1
  63. package/package.json +6 -1
  64. package/THIRD_PARTY_NOTICES.md +0 -15
@@ -0,0 +1,821 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'fs/promises';
2
+ import { dirname } from 'path';
3
+ import { proto } from '../../WAProto/index.js';
4
+ import { initAuthCreds } from '../Utils/auth-utils.js';
5
+ import { BufferJSON, generateMessageIDV2 } from '../Utils/generics.js';
6
+ import { normalizeMessageContent } from '../Utils/messages.js';
7
+ import { buildInteractiveBizNodes, jidNormalizedUser } from '../WABinary/index.js';
8
+ const isRecord = (value) => !!value && typeof value === 'object';
9
+ /**
10
+ * Walks common WhatsApp message wrappers without mutating the original object.
11
+ * This also supports WebMessageInfo-like objects as input.
12
+ */
13
+ export const unwrapMessage = (input) => {
14
+ if (!isRecord(input))
15
+ return undefined;
16
+ let current = isRecord(input.message) ? input.message : input;
17
+ const seen = new Set();
18
+ for (let i = 0; i < 20 && isRecord(current) && !seen.has(current); i++) {
19
+ seen.add(current);
20
+ const next = current.deviceSentMessage?.message ||
21
+ current.botForwardedMessage?.message ||
22
+ current.ephemeralMessage?.message ||
23
+ current.viewOnceMessage?.message ||
24
+ current.viewOnceMessageV2?.message ||
25
+ current.viewOnceMessageV2Extension?.message ||
26
+ current.documentWithCaptionMessage?.message ||
27
+ current.editedMessage?.message ||
28
+ current.associatedChildMessage?.message ||
29
+ current.groupStatusMessage?.message ||
30
+ current.groupStatusMessageV2?.message;
31
+ if (!isRecord(next) || next === current)
32
+ break;
33
+ current = next;
34
+ }
35
+ return current;
36
+ };
37
+ /** Decodes the AIRich protobuf payload used by Qiro's AIRich builder and compatible senders. */
38
+ export const readRichMessage = (input) => {
39
+ const root = unwrapMessage(input);
40
+ if (!root)
41
+ return null;
42
+ const rich = root.richResponseMessage || root.botForwardedMessage?.message?.richResponseMessage;
43
+ if (isRecord(rich)) {
44
+ const typeNames = {
45
+ 0: 'unknown', 1: 'grid-image', 2: 'text', 3: 'inline-image', 4: 'table', 5: 'code',
46
+ 6: 'dynamic', 7: 'map', 8: 'latex', 9: 'content-items'
47
+ };
48
+ const sections = (Array.isArray(rich.submessages) ? rich.submessages : []).map((section) => {
49
+ const messageType = Number(section?.messageType ?? 0);
50
+ return {
51
+ kind: typeNames[messageType] || 'unknown',
52
+ messageType,
53
+ text: section?.messageText,
54
+ gridImageMetadata: section?.gridImageMetadata,
55
+ imageMetadata: section?.imageMetadata,
56
+ codeMetadata: section?.codeMetadata,
57
+ tableMetadata: section?.tableMetadata,
58
+ dynamicMetadata: section?.dynamicMetadata,
59
+ latexMetadata: section?.latexMetadata,
60
+ mapMetadata: section?.mapMetadata,
61
+ contentItemsMetadata: section?.contentItemsMetadata
62
+ };
63
+ });
64
+ const title = typeof rich.unifiedResponse?.title === 'string' ? rich.unifiedResponse.title : undefined;
65
+ return { kind: 'ai-rich', messageType: rich.messageType, title, sections, contextInfo: rich.contextInfo, raw: rich };
66
+ }
67
+ const interactive = root.interactiveMessage;
68
+ if (isRecord(interactive)) {
69
+ const widget = interactive.bloksWidget;
70
+ const flow = interactive.nativeFlowMessage;
71
+ const sections = [];
72
+ if (Array.isArray(flow?.buttons)) {
73
+ for (const button of flow.buttons) {
74
+ let params = button?.buttonParamsJson;
75
+ if (typeof params === 'string') {
76
+ try {
77
+ params = JSON.parse(params);
78
+ }
79
+ catch { /* retain the original parameter string */ }
80
+ }
81
+ sections.push({ kind: 'button', name: button?.name, text: button?.text, params });
82
+ }
83
+ }
84
+ const title = interactive.header?.title || interactive.body?.text || interactive.footer?.text;
85
+ return {
86
+ kind: widget ? 'a2ui' : (flow ? 'interactive' : 'bloks'),
87
+ title: typeof title === 'string' ? title : undefined,
88
+ sections,
89
+ contextInfo: interactive.contextInfo,
90
+ raw: interactive
91
+ };
92
+ }
93
+ if (isRecord(root.bloksWidget)) {
94
+ return { kind: 'a2ui', title: root.bloksWidget.fallback, sections: [], raw: root.bloksWidget };
95
+ }
96
+ return null;
97
+ };
98
+ /** Reads social/link-card metadata from common text, video, and forwarded message shapes. */
99
+ export const readSocialPreview = (input) => {
100
+ const root = unwrapMessage(input);
101
+ if (!root)
102
+ return null;
103
+ const extended = root.extendedTextMessage || root.imageMessage || root.videoMessage || root.documentMessage || {};
104
+ const context = extended.contextInfo || root.messageContextInfo || {};
105
+ const external = context.externalAdReply || context.externalReply || {};
106
+ const link = context.linkPreview || context.externalAdReply || {};
107
+ const text = root.conversation || extended.text || root.videoMessage?.caption || root.imageMessage?.caption || '';
108
+ const candidateUrl = external.sourceUrl || external.mediaUrl || link['matched-text'] || link.canonicalUrl || link.canonical_url;
109
+ const foundUrl = typeof candidateUrl === 'string' ? candidateUrl : (String(text).match(/https?:\/\/[^\s<>]+/i)?.[0]);
110
+ const title = external.title || link.title || link['title'];
111
+ const description = external.body || link.description || link['description'];
112
+ const thumbnail = external.thumbnail || link.jpegThumbnail || link.highQualityThumbnail?.jpegThumbnail;
113
+ const thumbnailUrl = external.thumbnailUrl || link.thumbnailUrl || link.originalThumbnailUrl;
114
+ const hasAny = !!(foundUrl || title || description || thumbnail || thumbnailUrl || external.mediaType);
115
+ if (!hasAny)
116
+ return null;
117
+ const durationRaw = external.duration || link.duration || root.videoMessage?.seconds;
118
+ const duration = Number(durationRaw);
119
+ return {
120
+ url: foundUrl,
121
+ matchedText: typeof text === 'string' && text ? text : undefined,
122
+ title: typeof title === 'string' ? title : undefined,
123
+ description: typeof description === 'string' ? description : undefined,
124
+ thumbnailUrl: typeof thumbnailUrl === 'string' ? thumbnailUrl : undefined,
125
+ thumbnail: thumbnail instanceof Uint8Array ? thumbnail : undefined,
126
+ mediaType: Number.isFinite(Number(external.mediaType ?? link.mediaType)) ? Number(external.mediaType ?? link.mediaType) : undefined,
127
+ sourceType: typeof external.sourceType === 'string' ? external.sourceType : undefined,
128
+ hasVideo: !!(external.videoUrl || external.video || root.videoMessage || Number(external.mediaType) === 2),
129
+ durationSeconds: Number.isFinite(duration) && duration >= 0 ? duration : undefined,
130
+ raw: external && Object.keys(external).length ? external : link
131
+ };
132
+ };
133
+ /** Normalizes a phone number or PN JID, while leaving non-PN destinations untouched. */
134
+ export const normalizeUserJid = (input) => {
135
+ if (typeof input !== 'string' || !input.trim())
136
+ throw new TypeError('jid must be a non-empty string');
137
+ const value = input.trim();
138
+ if (value.includes('@')) {
139
+ if (value.endsWith('@g.us') || value.endsWith('@newsletter') || value === 'status@broadcast' || value.endsWith('@lid'))
140
+ return value;
141
+ return jidNormalizedUser(value);
142
+ }
143
+ const digits = value.replace(/\D/g, '');
144
+ if (digits.length < 5 || digits.length > 20)
145
+ throw new TypeError('phone number must contain 5–20 digits including country code');
146
+ return `${digits}@s.whatsapp.net`;
147
+ };
148
+ /** Builds a classic reply-button protobuf message. Legacy buttons can be hidden by some WhatsApp clients. */
149
+ export const buildButtonsMessage = (input) => {
150
+ if (!input || typeof input.text !== 'string' || !input.text.trim())
151
+ throw new TypeError('buttons message text is required');
152
+ if (!Array.isArray(input.buttons) || input.buttons.length < 1 || input.buttons.length > 3) {
153
+ throw new RangeError('classic buttons require between 1 and 3 buttons');
154
+ }
155
+ const seen = new Set();
156
+ const buttons = input.buttons.map((button, index) => {
157
+ const id = String(button?.id || '').trim();
158
+ const text = String(button?.text || '').trim();
159
+ if (!id || !text)
160
+ throw new TypeError(`button ${index + 1} requires a non-empty id and text`);
161
+ if (seen.has(id))
162
+ throw new TypeError(`duplicate button id: ${id}`);
163
+ if (text.length > 25)
164
+ throw new RangeError(`button ${index + 1} text must be 25 characters or fewer`);
165
+ seen.add(id);
166
+ return { buttonId: id, buttonText: { displayText: text }, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE };
167
+ });
168
+ return {
169
+ buttonsMessage: {
170
+ contentText: input.text,
171
+ text: input.title,
172
+ footerText: input.footer,
173
+ headerType: input.title ? proto.Message.ButtonsMessage.HeaderType.TEXT : proto.Message.ButtonsMessage.HeaderType.EMPTY,
174
+ buttons,
175
+ contextInfo: input.contextInfo
176
+ }
177
+ };
178
+ };
179
+ /** Builds a classic single-select list protobuf message and validates row limits before relay. */
180
+ export const buildListMessage = (input) => {
181
+ if (!input || typeof input.text !== 'string' || !input.text.trim())
182
+ throw new TypeError('list message text is required');
183
+ if (!String(input.buttonText || '').trim())
184
+ throw new TypeError('list buttonText is required');
185
+ if (!Array.isArray(input.sections) || input.sections.length < 1 || input.sections.length > 10) {
186
+ throw new RangeError('list requires between 1 and 10 sections');
187
+ }
188
+ let totalRows = 0;
189
+ const ids = new Set();
190
+ const sections = input.sections.map((section, sectionIndex) => {
191
+ if (!Array.isArray(section.rows) || section.rows.length < 1)
192
+ throw new TypeError(`section ${sectionIndex + 1} requires rows`);
193
+ const rows = section.rows.map((row, rowIndex) => {
194
+ totalRows++;
195
+ const id = String(row?.id || '').trim();
196
+ const title = String(row?.title || '').trim();
197
+ if (!id || !title)
198
+ throw new TypeError(`row ${sectionIndex + 1}.${rowIndex + 1} requires a non-empty id and title`);
199
+ if (ids.has(id))
200
+ throw new TypeError(`duplicate list row id: ${id}`);
201
+ if (title.length > 24)
202
+ throw new RangeError(`row title ${sectionIndex + 1}.${rowIndex + 1} must be 24 characters or fewer`);
203
+ ids.add(id);
204
+ return { rowId: id, title, description: row.description };
205
+ });
206
+ return { title: section.title, rows };
207
+ });
208
+ if (totalRows > 10)
209
+ throw new RangeError('classic lists support at most 10 rows total');
210
+ return {
211
+ listMessage: {
212
+ title: input.title,
213
+ description: input.text,
214
+ buttonText: input.buttonText,
215
+ footerText: input.footer,
216
+ listType: proto.Message.ListMessage.ListType.SINGLE_SELECT,
217
+ sections,
218
+ contextInfo: input.contextInfo
219
+ }
220
+ };
221
+ };
222
+ /** Relays a prebuilt protobuf message through the current Baileys socket. */
223
+ export const sendClassicMessage = async (socket, jid, message, options = {}) => {
224
+ if (!socket || typeof socket.relayMessage !== 'function')
225
+ throw new TypeError('a connected Qiro Baileys socket is required');
226
+ if (!jid)
227
+ throw new TypeError('jid is required');
228
+ const normalized = normalizeMessageContent(message);
229
+ const additionalNodes = options.autoBizNode !== false && jid !== 'status@broadcast' && !jid.endsWith('@newsletter')
230
+ ? buildInteractiveBizNodes(normalized)
231
+ : [];
232
+ return socket.relayMessage(jid, message, {
233
+ messageId: options.messageId || generateMessageIDV2(socket.user?.id || undefined),
234
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
235
+ statusJidList: options.statusJidList,
236
+ isSecret: options.isSecret,
237
+ protected: options.protected,
238
+ me: options.me,
239
+ additionalNodes
240
+ });
241
+ };
242
+ /** Sends media as a WhatsApp album using the album parent key already supported by Qiro Baileys. */
243
+ export const sendAlbum = async (socket, jid, items, options = {}) => {
244
+ if (!Array.isArray(items) || items.length < 2 || items.length > 10) {
245
+ throw new RangeError('an album requires between 2 and 10 image/video items');
246
+ }
247
+ const imageCount = items.filter(item => 'image' in item).length;
248
+ const videoCount = items.length - imageCount;
249
+ if (imageCount + videoCount !== items.length)
250
+ throw new TypeError('album items must be image or video messages');
251
+ const { onProgress, ...sendOptions } = options;
252
+ const parent = await socket.sendMessage(jid, { album: { expectedImageCount: imageCount, expectedVideoCount: videoCount } }, sendOptions);
253
+ if (!parent?.key?.id)
254
+ throw new Error('WhatsApp did not return an album parent message key');
255
+ const messages = [];
256
+ for (let index = 0; index < items.length; index++) {
257
+ const item = items[index];
258
+ const child = { ...item, albumParentKey: parent.key };
259
+ const sent = await socket.sendMessage(jid, child, sendOptions);
260
+ if (!sent)
261
+ throw new Error(`WhatsApp did not return album item ${index + 1}; ${messages.length} item(s) already sent`);
262
+ messages.push(sent);
263
+ onProgress?.(index + 1, items.length);
264
+ }
265
+ return { parent, messages };
266
+ };
267
+ /** Sends a link-card using WhatsApp's externalAdReply context. The client decides whether to render it. */
268
+ export const richLink = async (socket, jid, options) => {
269
+ let parsed;
270
+ try {
271
+ parsed = new URL(options.url);
272
+ }
273
+ catch {
274
+ throw new TypeError('richLink url must be an absolute URL');
275
+ }
276
+ if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password)
277
+ throw new TypeError('richLink requires an http/https URL without embedded credentials');
278
+ const externalAdReply = {
279
+ title: options.title,
280
+ body: options.description || '',
281
+ sourceUrl: parsed.toString(),
282
+ mediaUrl: parsed.toString(),
283
+ thumbnailUrl: options.thumbnailUrl,
284
+ thumbnail: options.thumbnail,
285
+ mediaType: options.mediaType ?? 1,
286
+ renderLargerThumbnail: options.large ?? true,
287
+ showAdAttribution: false
288
+ };
289
+ return socket.sendMessage(jid, {
290
+ text: options.text || options.description || options.title || parsed.toString(),
291
+ contextInfo: { externalAdReply }
292
+ }, options.quoted ? { quoted: options.quoted } : {});
293
+ };
294
+ /** Adds convenience methods without replacing or wrapping core socket methods. */
295
+ export const attachAdvancedSocketMethods = (socket, autoBizNode = true) => {
296
+ const extended = socket;
297
+ extended.sendAlbum = (jid, items, options) => sendAlbum(socket, jid, items, options);
298
+ extended.sendClassicMessage = (jid, message, options) => sendClassicMessage(socket, jid, message, { ...options, autoBizNode: options?.autoBizNode ?? autoBizNode });
299
+ extended.richLink = (jid, options) => richLink(socket, jid, options);
300
+ extended.sendStatus = (content, recipients = [], options = {}) => {
301
+ if (!Array.isArray(recipients) || recipients.length === 0) {
302
+ throw new TypeError('sendStatus requires an explicit, non-empty recipient list');
303
+ }
304
+ return socket.sendMessage('status@broadcast', content, {
305
+ ...options,
306
+ statusJidList: recipients.map(normalizeUserJid)
307
+ });
308
+ };
309
+ return extended;
310
+ };
311
+ export const makeInMemoryStore = (options = {}) => {
312
+ const maxMessages = Math.max(50, Math.floor(options.maxMessagesPerChat || 1000));
313
+ const chats = new Map();
314
+ const contacts = new Map();
315
+ const groupMetadata = new Map();
316
+ const messages = new Map();
317
+ const messageIndex = new Map();
318
+ let boundEvents;
319
+ let boundHandlers = [];
320
+ let fileWriteQueue = Promise.resolve();
321
+ const makeMessageKey = (jid, id) => `${jid}\u0000${id}`;
322
+ const addMessage = (message) => {
323
+ const jid = message?.key?.remoteJid;
324
+ const id = message?.key?.id;
325
+ if (!jid || !id)
326
+ return;
327
+ const key = makeMessageKey(jid, id);
328
+ const previous = messageIndex.get(key);
329
+ if (previous)
330
+ Object.assign(previous, message);
331
+ else
332
+ messageIndex.set(key, message);
333
+ const list = messages.get(jid) || [];
334
+ if (!previous)
335
+ list.push(messageIndex.get(key));
336
+ if (list.length > maxMessages) {
337
+ const removed = list.splice(0, list.length - maxMessages);
338
+ for (const old of removed)
339
+ if (old.key?.id)
340
+ messageIndex.delete(makeMessageKey(jid, old.key.id));
341
+ }
342
+ messages.set(jid, list);
343
+ };
344
+ const store = {
345
+ chats,
346
+ contacts,
347
+ groupMetadata,
348
+ messages,
349
+ bind(ev) {
350
+ if (boundEvents)
351
+ this.unbind();
352
+ boundEvents = ev;
353
+ boundHandlers = [];
354
+ const add = (event, fn) => { ev.on(event, fn); boundHandlers.push([event, fn]); };
355
+ add('messages.upsert', (data) => (data?.messages || []).forEach(addMessage));
356
+ add('messages.update', (updates) => {
357
+ for (const update of updates || []) {
358
+ const jid = update?.key?.remoteJid;
359
+ const id = update?.key?.id;
360
+ if (!jid || !id)
361
+ continue;
362
+ const current = messageIndex.get(makeMessageKey(jid, id));
363
+ if (current) {
364
+ if (update.update?.message)
365
+ current.message = update.update.message;
366
+ Object.assign(current, Object.fromEntries(Object.entries(update.update || {}).filter(([k]) => k !== 'message')));
367
+ }
368
+ }
369
+ });
370
+ add('messages.delete', (data) => {
371
+ const keys = Array.isArray(data) ? data : data?.keys || [];
372
+ for (const key of keys) {
373
+ if (!key.remoteJid || !key.id)
374
+ continue;
375
+ messageIndex.delete(makeMessageKey(key.remoteJid, key.id));
376
+ messages.set(key.remoteJid, (messages.get(key.remoteJid) || []).filter(m => m.key.id !== key.id));
377
+ }
378
+ });
379
+ add('chats.upsert', (rows) => (rows || []).forEach(row => { if (row?.id)
380
+ chats.set(row.id, { ...(chats.get(row.id) || {}), ...row }); }));
381
+ add('chats.update', (rows) => (rows || []).forEach(row => { if (row?.id)
382
+ chats.set(row.id, { ...(chats.get(row.id) || {}), ...row }); }));
383
+ add('chats.delete', (ids) => (ids || []).forEach(id => chats.delete(id)));
384
+ add('contacts.upsert', (rows) => (rows || []).forEach(row => { if (row?.id)
385
+ contacts.set(row.id, { ...(contacts.get(row.id) || {}), ...row }); }));
386
+ add('contacts.update', (rows) => (rows || []).forEach(row => { if (row?.id)
387
+ contacts.set(row.id, { ...(contacts.get(row.id) || {}), ...row }); }));
388
+ add('groups.update', (rows) => (rows || []).forEach(row => { if (row?.id)
389
+ groupMetadata.set(row.id, { ...(groupMetadata.get(row.id) || {}), ...row }); }));
390
+ add('groups.upsert', (rows) => (rows || []).forEach(row => { if (row?.id)
391
+ groupMetadata.set(row.id, { ...(groupMetadata.get(row.id) || {}), ...row }); }));
392
+ return this;
393
+ },
394
+ unbind() {
395
+ for (const [event, handler] of boundHandlers) {
396
+ if (typeof boundEvents?.off === 'function')
397
+ boundEvents.off(event, handler);
398
+ else
399
+ boundEvents?.removeListener?.(event, handler);
400
+ }
401
+ boundHandlers = [];
402
+ boundEvents = undefined;
403
+ },
404
+ loadMessage(jid, id) { return messageIndex.get(makeMessageKey(jid, id)); },
405
+ async readFromFile(path) {
406
+ const parsed = JSON.parse(await readFile(path, 'utf8'), BufferJSON.reviver);
407
+ chats.clear();
408
+ contacts.clear();
409
+ groupMetadata.clear();
410
+ messages.clear();
411
+ messageIndex.clear();
412
+ for (const item of parsed.chats || [])
413
+ chats.set(item[0], item[1]);
414
+ for (const item of parsed.contacts || [])
415
+ contacts.set(item[0], item[1]);
416
+ for (const item of parsed.groupMetadata || [])
417
+ groupMetadata.set(item[0], item[1]);
418
+ for (const item of parsed.messages || [])
419
+ for (const message of item[1] || [])
420
+ addMessage(message);
421
+ return this;
422
+ },
423
+ async writeToFile(path) {
424
+ const task = fileWriteQueue.then(async () => {
425
+ await mkdir(dirname(path), { recursive: true });
426
+ const tmp = `${path}.${process.pid}.tmp`;
427
+ const payload = { chats: [...chats], contacts: [...contacts], groupMetadata: [...groupMetadata], messages: [...messages] };
428
+ await writeFile(tmp, JSON.stringify(payload, BufferJSON.replacer));
429
+ await rename(tmp, path);
430
+ });
431
+ fileWriteQueue = task.catch(() => undefined);
432
+ return task;
433
+ }
434
+ };
435
+ return store;
436
+ };
437
+ const createAuthState = async (adapter) => {
438
+ const storedCreds = (await adapter.get('creds', 'current'));
439
+ const creds = storedCreds || initAuthCreds();
440
+ if (!storedCreds)
441
+ await adapter.set('creds', 'current', creds);
442
+ const keys = {
443
+ get: async (type, ids) => {
444
+ const entries = await Promise.all(ids.map(async (id) => [id, await adapter.get(type, id)]));
445
+ const result = {};
446
+ for (const [id, value] of entries) {
447
+ if (value && type === 'app-state-sync-key')
448
+ result[id] = proto.Message.AppStateSyncKeyData.fromObject(value);
449
+ else
450
+ result[id] = value;
451
+ }
452
+ return result;
453
+ },
454
+ set: async (data) => {
455
+ const apply = async () => {
456
+ for (const category of Object.keys(data)) {
457
+ const values = data[category];
458
+ for (const [id, value] of Object.entries(values || {})) {
459
+ if (value == null)
460
+ await adapter.delete(category, id);
461
+ else
462
+ await adapter.set(category, id, value);
463
+ }
464
+ }
465
+ };
466
+ if (adapter.transaction)
467
+ await adapter.transaction(apply);
468
+ else
469
+ await apply();
470
+ }
471
+ };
472
+ return { state: { creds, keys }, saveCreds: () => adapter.set('creds', 'current', creds) };
473
+ };
474
+ /** Stores the complete auth state in one JSON file, with atomic replacement on each write. */
475
+ export const useSingleFileAuthState = async (filePath) => {
476
+ await mkdir(dirname(filePath), { recursive: true });
477
+ let snapshot = {};
478
+ try {
479
+ snapshot = JSON.parse(await readFile(filePath, 'utf8'), BufferJSON.reviver);
480
+ }
481
+ catch (error) {
482
+ if (error?.code !== 'ENOENT') {
483
+ throw new Error(`Could not read auth state file ${filePath}: ${error.message}`);
484
+ }
485
+ }
486
+ let persistQueue = Promise.resolve();
487
+ const persist = async () => {
488
+ const task = persistQueue.then(async () => {
489
+ const temp = `${filePath}.${process.pid}.tmp`;
490
+ await writeFile(temp, JSON.stringify(snapshot, BufferJSON.replacer));
491
+ await rename(temp, filePath);
492
+ });
493
+ persistQueue = task.catch(() => undefined);
494
+ return task;
495
+ };
496
+ const adapter = {
497
+ get: async (category, id) => snapshot[category]?.[id] ?? null,
498
+ set: async (category, id, value) => { snapshot[category] || (snapshot[category] = {}); snapshot[category][id] = value; await persist(); },
499
+ delete: async (category, id) => { if (snapshot[category])
500
+ delete snapshot[category][id]; await persist(); }
501
+ };
502
+ return createAuthState(adapter);
503
+ };
504
+ /** Built-in Node SQLite auth adapter; requires Node.js 22.5+ and does not use better-sqlite3. */
505
+ export const useSqliteAuthState = async (filePath, sessionId = 'default') => {
506
+ if (!sessionId.trim())
507
+ throw new TypeError('sessionId must not be empty');
508
+ let sqlite;
509
+ try {
510
+ const specifier = 'node:sqlite';
511
+ sqlite = await import(specifier);
512
+ }
513
+ catch (error) {
514
+ throw new Error(`useSqliteAuthState requires Node.js 22.5+ with node:sqlite. Original error: ${error.message}`);
515
+ }
516
+ const DatabaseSync = sqlite.DatabaseSync;
517
+ await mkdir(dirname(filePath), { recursive: true });
518
+ const db = new DatabaseSync(filePath);
519
+ db.exec('CREATE TABLE IF NOT EXISTS qiro_baileys_auth (session_id TEXT NOT NULL, category TEXT NOT NULL, data_id TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY(session_id, category, data_id))');
520
+ const getStmt = db.prepare('SELECT value FROM qiro_baileys_auth WHERE session_id = ? AND category = ? AND data_id = ?');
521
+ const setStmt = db.prepare('INSERT INTO qiro_baileys_auth(session_id, category, data_id, value) VALUES(?, ?, ?, ?) ON CONFLICT(session_id, category, data_id) DO UPDATE SET value = excluded.value');
522
+ const deleteStmt = db.prepare('DELETE FROM qiro_baileys_auth WHERE session_id = ? AND category = ? AND data_id = ?');
523
+ let transactionQueue = Promise.resolve();
524
+ const adapter = {
525
+ get: async (category, id) => {
526
+ const row = getStmt.get(sessionId, category, id);
527
+ if (!row?.value)
528
+ return null;
529
+ return JSON.parse(row.value, BufferJSON.reviver);
530
+ },
531
+ set: async (category, id, value) => { setStmt.run(sessionId, category, id, JSON.stringify(value, BufferJSON.replacer)); },
532
+ delete: async (category, id) => { deleteStmt.run(sessionId, category, id); },
533
+ transaction: async (work) => {
534
+ const task = transactionQueue.then(async () => {
535
+ db.exec('BEGIN IMMEDIATE');
536
+ try {
537
+ await work();
538
+ db.exec('COMMIT');
539
+ }
540
+ catch (error) {
541
+ db.exec('ROLLBACK');
542
+ throw error;
543
+ }
544
+ });
545
+ transactionQueue = task.catch(() => undefined);
546
+ return task;
547
+ },
548
+ close: () => db.close()
549
+ };
550
+ const result = await createAuthState(adapter);
551
+ return { ...result, close: () => db.close() };
552
+ };
553
+ const safeSqlName = (name) => {
554
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
555
+ throw new TypeError(`unsafe SQL identifier: ${name}`);
556
+ return name;
557
+ };
558
+ /** PostgreSQL adapter. Pass an already-connected pg Pool or Client. */
559
+ export const usePostgresAuthState = async (pool, options = {}) => {
560
+ if (!pool?.query)
561
+ throw new TypeError('usePostgresAuthState expects a connected pg Pool/Client');
562
+ const session = options.sessionId || 'default';
563
+ const table = safeSqlName(options.tableName || 'qiro_baileys_auth');
564
+ await pool.query(`CREATE TABLE IF NOT EXISTS ${table} (session_id TEXT NOT NULL, category TEXT NOT NULL, data_id TEXT NOT NULL, value JSONB NOT NULL, PRIMARY KEY(session_id, category, data_id))`);
565
+ const adapter = {
566
+ get: async (category, id) => {
567
+ const value = (await pool.query(`SELECT value FROM ${table} WHERE session_id=$1 AND category=$2 AND data_id=$3`, [session, category, id])).rows?.[0]?.value;
568
+ return value == null ? null : JSON.parse(JSON.stringify(value), BufferJSON.reviver);
569
+ },
570
+ set: async (category, id, value) => { await pool.query(`INSERT INTO ${table}(session_id,category,data_id,value) VALUES($1,$2,$3,$4::jsonb) ON CONFLICT(session_id,category,data_id) DO UPDATE SET value=EXCLUDED.value`, [session, category, id, JSON.stringify(value, BufferJSON.replacer)]); },
571
+ delete: async (category, id) => { await pool.query(`DELETE FROM ${table} WHERE session_id=$1 AND category=$2 AND data_id=$3`, [session, category, id]); }
572
+ };
573
+ return createAuthState(adapter);
574
+ };
575
+ /** MySQL/MariaDB adapter. Pass an already-connected mysql2-compatible pool/connection. */
576
+ export const useMySQLAuthState = async (pool, options = {}) => {
577
+ if (!pool?.query && !pool?.execute)
578
+ throw new TypeError('useMySQLAuthState expects a connected mysql2-compatible pool/connection');
579
+ const session = options.sessionId || 'default';
580
+ const table = safeSqlName(options.tableName || 'qiro_baileys_auth');
581
+ const run = (sql, values = []) => (pool.execute || pool.query).call(pool, sql, values);
582
+ await run(`CREATE TABLE IF NOT EXISTS \`${table}\` (session_id VARCHAR(128) NOT NULL, category VARCHAR(128) NOT NULL, data_id VARCHAR(255) NOT NULL, value LONGTEXT NOT NULL, PRIMARY KEY(session_id, category, data_id))`);
583
+ const rowsOf = (result) => Array.isArray(result?.[0]) ? result[0] : result;
584
+ const adapter = {
585
+ get: async (category, id) => {
586
+ const result = await run(`SELECT value FROM \`${table}\` WHERE session_id=? AND category=? AND data_id=? LIMIT 1`, [session, category, id]);
587
+ const row = rowsOf(result)?.[0];
588
+ return row?.value ? JSON.parse(row.value, BufferJSON.reviver) : null;
589
+ },
590
+ set: async (category, id, value) => { await run(`INSERT INTO \`${table}\`(session_id,category,data_id,value) VALUES(?,?,?,?) ON DUPLICATE KEY UPDATE value=VALUES(value)`, [session, category, id, JSON.stringify(value, BufferJSON.replacer)]); },
591
+ delete: async (category, id) => { await run(`DELETE FROM \`${table}\` WHERE session_id=? AND category=? AND data_id=?`, [session, category, id]); }
592
+ };
593
+ return createAuthState(adapter);
594
+ };
595
+ /** MongoDB adapter. Pass a connected Db instance from the official mongodb driver. */
596
+ export const useMongoAuthState = async (db, options = {}) => {
597
+ if (!db?.collection)
598
+ throw new TypeError('useMongoAuthState expects a connected MongoDB Db instance');
599
+ const session = options.sessionId || 'default';
600
+ const collection = db.collection(options.collectionName || 'qiro_baileys_auth');
601
+ if (typeof collection.createIndex === 'function') {
602
+ try {
603
+ await collection.createIndex({ session_id: 1, category: 1, data_id: 1 }, { unique: true });
604
+ }
605
+ catch { /* index may already exist or the deployment may manage indexes centrally */ }
606
+ }
607
+ const adapter = {
608
+ get: async (category, id) => {
609
+ const value = (await collection.findOne({ session_id: session, category, data_id: id }))?.value;
610
+ return value == null ? null : JSON.parse(typeof value === 'string' ? value : JSON.stringify(value), BufferJSON.reviver);
611
+ },
612
+ set: async (category, id, value) => { await collection.updateOne({ session_id: session, category, data_id: id }, { $set: { session_id: session, category, data_id: id, value: JSON.stringify(value, BufferJSON.replacer) } }, { upsert: true }); },
613
+ delete: async (category, id) => { await collection.deleteOne({ session_id: session, category, data_id: id }); }
614
+ };
615
+ return createAuthState(adapter);
616
+ };
617
+ /** Redis adapter. Supports node-redis and ioredis-style hash methods. */
618
+ export const useRedisAuthState = async (client, options = {}) => {
619
+ if (!client)
620
+ throw new TypeError('useRedisAuthState expects a connected Redis client');
621
+ const session = options.sessionId || 'default';
622
+ const prefix = options.prefix || 'qiro:baileys-auth';
623
+ const hget = client.hGet?.bind(client) || client.hget?.bind(client);
624
+ const hset = client.hSet?.bind(client) || client.hset?.bind(client);
625
+ const hdel = client.hDel?.bind(client) || client.hdel?.bind(client);
626
+ if (!hget || !hset || !hdel)
627
+ throw new TypeError('Redis client must expose hGet/hSet/hDel or hget/hset/hdel');
628
+ const hash = (category) => `${prefix}:${session}:${category}`;
629
+ const adapter = {
630
+ get: async (category, id) => { const value = await hget(hash(category), id); return value == null ? null : JSON.parse(value, BufferJSON.reviver); },
631
+ set: async (category, id, value) => { await hset(hash(category), id, JSON.stringify(value, BufferJSON.replacer)); },
632
+ delete: async (category, id) => { await hdel(hash(category), id); }
633
+ };
634
+ return createAuthState(adapter);
635
+ };
636
+ export const useCacheManagerAuthState = async (cache, options = {}) => {
637
+ if (!cache?.get || !cache?.set || !(cache.del || cache.delete)) {
638
+ throw new TypeError('useCacheManagerAuthState expects a cache-manager-like get/set/del adapter');
639
+ }
640
+ const session = options.sessionId || 'default';
641
+ const prefix = options.prefix || 'qiro:baileys-auth';
642
+ const deleteValue = (cache.del || cache.delete).bind(cache);
643
+ const key = (category, id) => `${prefix}:${session}:${category}:${id}`;
644
+ const adapter = {
645
+ get: async (category, id) => {
646
+ const value = await cache.get(key(category, id));
647
+ if (value == null)
648
+ return null;
649
+ return typeof value === 'string' ? JSON.parse(value, BufferJSON.reviver) : value;
650
+ },
651
+ set: async (category, id, value) => { await cache.set(key(category, id), JSON.stringify(value, BufferJSON.replacer)); },
652
+ delete: async (category, id) => { await deleteValue(key(category, id)); }
653
+ };
654
+ return createAuthState(adapter);
655
+ };
656
+ export const makeCacheManagerAuthState = useCacheManagerAuthState;
657
+ /** Generic adapter bridge for NekoDB or another async key-value backend. */
658
+ export const useNekoDBAuth = async (adapterLike, namespace = 'qiro_baileys_auth') => {
659
+ if (!adapterLike)
660
+ throw new TypeError('useNekoDBAuth expects an adapter/database instance');
661
+ const getNative = adapterLike.get?.bind(adapterLike);
662
+ const setNative = adapterLike.set?.bind(adapterLike);
663
+ const deleteNative = (adapterLike.delete || adapterLike.del)?.bind(adapterLike);
664
+ if (!getNative || !setNative || !deleteNative)
665
+ throw new TypeError('NekoDB adapter must expose async get, set, and delete/del methods');
666
+ const adapter = {
667
+ get: async (category, id) => {
668
+ const value = await getNative(`${namespace}:${category}:${id}`);
669
+ return typeof value === 'string' ? JSON.parse(value, BufferJSON.reviver) : value ?? null;
670
+ },
671
+ set: async (category, id, value) => { await setNative(`${namespace}:${category}:${id}`, JSON.stringify(value, BufferJSON.replacer)); },
672
+ delete: async (category, id) => { await deleteNative(`${namespace}:${category}:${id}`); }
673
+ };
674
+ return createAuthState(adapter);
675
+ };
676
+ /** Common database-backed store contract for integrations without a built-in adapter. */
677
+ export const useAdapterAuthState = createAuthState;
678
+ const loadFfmpeg = async () => {
679
+ try {
680
+ const moduleName = 'fluent-ffmpeg';
681
+ const mod = await import(moduleName);
682
+ return mod.default || mod;
683
+ }
684
+ catch {
685
+ throw new Error('This media helper requires fluent-ffmpeg and an FFmpeg binary installed on the host');
686
+ }
687
+ };
688
+ const ffmpegInput = (ffmpeg, input) => {
689
+ if (typeof input !== 'string' || !input.trim())
690
+ throw new TypeError('FFmpeg helpers require a non-empty file path or URL');
691
+ return ffmpeg(input);
692
+ };
693
+ /** Inspect codecs, container, streams, and duration via ffprobe. */
694
+ export const probeMedia = async (input) => {
695
+ const ffmpeg = await loadFfmpeg();
696
+ return new Promise((resolve, reject) => ffmpeg.ffprobe(input, (error, data) => error ? reject(error) : resolve(data)));
697
+ };
698
+ /** Converts a file to an MP4/H.264 WhatsApp-oriented profile. */
699
+ export const convertToWhatsAppVideo = async (input, outputPath, options = {}) => {
700
+ if (!outputPath)
701
+ throw new TypeError('outputPath is required');
702
+ const ffmpeg = await loadFfmpeg();
703
+ const { width, height, crf = 23, preset = 'medium', audioBitrate = '128k' } = options;
704
+ return new Promise((resolve, reject) => {
705
+ let command = ffmpegInput(ffmpeg, input).videoCodec('libx264').audioCodec('aac').format('mp4')
706
+ .outputOptions(['-movflags +faststart', `-crf ${crf}`, `-preset ${preset}`, `-b:a ${audioBitrate}`]);
707
+ if (width || height)
708
+ command = command.size(`${width || '?'}x${height || '?'}`).autopad();
709
+ command.on('error', reject).on('end', () => resolve(outputPath)).save(outputPath);
710
+ });
711
+ };
712
+ /** Converts an audio file to OGG/Opus, suitable for voice-note/audio delivery. */
713
+ export const convertToOpusAudio = async (input, outputPath, options = {}) => {
714
+ if (!outputPath)
715
+ throw new TypeError('outputPath is required');
716
+ const ffmpeg = await loadFfmpeg();
717
+ const { bitrate = '64k', sampleRate = 48000, channels = 1 } = options;
718
+ return new Promise((resolve, reject) => {
719
+ ffmpegInput(ffmpeg, input).noVideo().audioCodec('libopus').audioBitrate(bitrate).audioFrequency(sampleRate).audioChannels(channels)
720
+ .format('ogg').on('error', reject).on('end', () => resolve(outputPath)).save(outputPath);
721
+ });
722
+ };
723
+ /** Extracts one video thumbnail with FFmpeg and returns the output file path. */
724
+ export const getVideoThumbnail = async (input, outputPath, options = {}) => {
725
+ const ffmpeg = await loadFfmpeg();
726
+ const timestamp = options.timestamp || '00:00:01.000';
727
+ const width = Math.max(64, Math.min(1920, Math.floor(options.width || 640)));
728
+ return new Promise((resolve, reject) => {
729
+ ffmpeg(input).screenshots({ timestamps: [timestamp], filename: outputPath.split(/[\\/]/).pop(), folder: dirname(outputPath), size: `${width}x?` })
730
+ .on('error', reject).on('end', () => resolve(outputPath));
731
+ });
732
+ };
733
+ /** Resizes an image buffer or path; lazily loads sharp, then falls back to Jimp. Returns a Buffer. */
734
+ export const resizeImage = async (input, options) => {
735
+ if (!options || !Number.isFinite(options.width) || options.width < 1)
736
+ throw new TypeError('resizeImage requires width >= 1');
737
+ const buffer = Buffer.isBuffer(input) ? input : input instanceof Uint8Array ? Buffer.from(input) : await readFile(input);
738
+ const width = Math.floor(options.width);
739
+ const height = options.height && options.height > 0 ? Math.floor(options.height) : undefined;
740
+ const quality = Math.max(1, Math.min(100, Math.floor(options.quality ?? 85)));
741
+ try {
742
+ const moduleName = 'sharp';
743
+ const imported = await import(moduleName);
744
+ let image = (imported.default || imported)(buffer).resize(width, height, { fit: options.fit || 'inside', withoutEnlargement: true });
745
+ if (options.format === 'png')
746
+ image = image.png();
747
+ else if (options.format === 'webp')
748
+ image = image.webp({ quality });
749
+ else
750
+ image = image.jpeg({ quality });
751
+ return await image.toBuffer();
752
+ }
753
+ catch (sharpError) {
754
+ try {
755
+ const moduleName = 'jimp';
756
+ const imported = await import(moduleName);
757
+ const Jimp = imported.Jimp || imported.default?.Jimp;
758
+ if (!Jimp)
759
+ throw sharpError;
760
+ const image = await Jimp.read(buffer);
761
+ image.resize({ w: width, h: height || Math.round(image.height * width / image.width) });
762
+ if (options.format === 'png')
763
+ return await image.getBuffer('image/png');
764
+ if (options.format === 'webp')
765
+ throw new Error('Jimp fallback does not support WebP output in this configuration');
766
+ return await image.getBuffer('image/jpeg', { quality });
767
+ }
768
+ catch {
769
+ throw new Error('resizeImage requires the optional sharp or jimp package to be installed');
770
+ }
771
+ }
772
+ };
773
+ /** Reads an MP4/MOV movie-header duration directly from its atoms without FFmpeg. */
774
+ export const getMp4Duration = async (input) => {
775
+ const buffer = Buffer.isBuffer(input) ? input : input instanceof Uint8Array ? Buffer.from(input) : await readFile(input);
776
+ const readDuration = (atomType) => {
777
+ let offset = 0;
778
+ while ((offset = buffer.indexOf(atomType, offset, 'ascii')) >= 0) {
779
+ const start = offset - 4;
780
+ offset += 4;
781
+ if (start < 0 || start + 8 > buffer.length)
782
+ continue;
783
+ const size = buffer.readUInt32BE(start);
784
+ if (size < 20 || start + size > buffer.length)
785
+ continue;
786
+ const dataStart = start + 8;
787
+ const version = buffer[dataStart];
788
+ try {
789
+ if (version === 0 && size >= 24) {
790
+ const timescale = buffer.readUInt32BE(dataStart + 12);
791
+ const duration = buffer.readUInt32BE(dataStart + 16);
792
+ if (timescale > 0)
793
+ return duration / timescale;
794
+ }
795
+ else if (version === 1 && size >= 32) {
796
+ const timescale = buffer.readUInt32BE(dataStart + 20);
797
+ const duration = Number(buffer.readBigUInt64BE(dataStart + 24));
798
+ if (timescale > 0)
799
+ return duration / timescale;
800
+ }
801
+ }
802
+ catch { /* try next matching atom */ }
803
+ }
804
+ return null;
805
+ };
806
+ const duration = readDuration('mvhd') ?? readDuration('mdhd');
807
+ if (duration == null || !Number.isFinite(duration) || duration < 0)
808
+ throw new Error('Could not read MP4/MOV duration; input may be fragmented, malformed, or not a supported file');
809
+ return duration;
810
+ };
811
+ /** View-once V2 flags are deliberately applied in the generation pipeline, not faked at the API surface. */
812
+ export const inspectMessageContent = (message) => {
813
+ const normalized = normalizeMessageContent(message);
814
+ return {
815
+ contentType: normalized ? Object.keys(normalized).find(key => key === 'conversation' || key.endsWith('Message')) || 'unknown' : 'unknown',
816
+ normalized,
817
+ socialPreview: readSocialPreview(normalized),
818
+ richMessage: readRichMessage(normalized)
819
+ };
820
+ };
821
+ //# sourceMappingURL=advanced.js.map