@innovatorssoft/innovators-bot2 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,1494 @@
1
+ const {
2
+ makeWASocket,
3
+ Browsers,
4
+ useMultiFileAuthState,
5
+ DisconnectReason,
6
+ fetchLatestBaileysVersion,
7
+ fetchLatestWaWebVersion,
8
+ downloadMediaMessage,
9
+ getCurrentSenderInfo,
10
+ // JID Utilities
11
+ parseJid,
12
+ plotJid,
13
+ normalizePhoneToJid,
14
+ // Anti-Delete
15
+ MessageStore,
16
+ createMessageStoreHandler,
17
+ createAntiDeleteHandler
18
+ } = require('@innovatorssoft/baileys');
19
+
20
+ const { Sticker, StickerTypes } = require('wa-sticker-formatter');
21
+
22
+ const { Boom } = require('@hapi/boom');
23
+ const { EventEmitter } = require('events');
24
+ const P = require('pino');
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const mime = require('mime');
28
+ const figlet = require('figlet');
29
+ const NodeCache = require('node-cache');
30
+
31
+ process.title = 'INNOVATORS Soft WhatsApp Server 92 322 4559543'
32
+
33
+ console.log(figlet.textSync('WELCOME To'))
34
+ console.log(figlet.textSync('INNOVATORS'))
35
+ console.log(figlet.textSync('SOFT'))
36
+
37
+ class Group {
38
+ constructor(client, groupData) {
39
+ this.client = client
40
+ this.id = groupData.id
41
+ this.subject = groupData.subject
42
+ this.creation = groupData.creation
43
+ this.owner = groupData.owner
44
+ this.desc = groupData.desc
45
+ this.participants = groupData.participants
46
+ }
47
+ }
48
+
49
+ class WhatsAppClient extends EventEmitter {
50
+ constructor(config = {}) {
51
+ super()
52
+ this.sock = null
53
+ this.isConnected = false
54
+ this.sessionName = config.sessionName || 'auth_info_baileys'
55
+ this._connectionState = 'disconnected'
56
+ this.authmethod = config.authmethod || 'qr';
57
+ this._reconnectDelay = 5000;
58
+ this.pairingPhoneNumber = config.pairingPhoneNumber || null;
59
+ this.groupMetadataCache = new NodeCache({ stdTTL: 600, checkperiod: 120 });
60
+ this.messageStore = new MessageStore({
61
+ maxMessagesPerChat: config.maxMessagesPerChat || 1000,
62
+ ttl: config.messageTTL || 24 * 60 * 60 * 1000
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Helper method to resolve LID to PN (Phone Number) if available and normalize JID
68
+ * @param {string} jid - The JID to resolve (could be LID or PN)
69
+ * @returns {Promise<string>} The resolved and normalized PN if LID mapping exists, otherwise the normalized original JID
70
+ * @private
71
+ */
72
+ async _resolveLidToPn(jid) {
73
+ if (!jid) return jid;
74
+
75
+ // If it's a LID, try to resolve it to PN
76
+ if (jid.endsWith('@lid')) {
77
+ try {
78
+ const phoneNumber = await this.getPNForLID(jid);
79
+ if (phoneNumber) {
80
+ // Normalize the resolved PN by removing device ID
81
+ return this._normalizeJid(phoneNumber);
82
+ }
83
+ return jid; // Return original LID if no PN found
84
+ } catch (error) {
85
+ console.error('Error resolving LID to PN:', error);
86
+ return jid; // Return original JID on error
87
+ }
88
+ }
89
+
90
+ // If it's already a PN or other format, normalize and return
91
+ return this._normalizeJid(jid);
92
+ }
93
+
94
+ /**
95
+ * Normalize JID by removing device ID suffix (e.g., :0)
96
+ * Converts 923014434335:0@s.whatsapp.net to 923014434335@s.whatsapp.net
97
+ * @param {string} jid - The JID to normalize
98
+ * @returns {string} Normalized JID
99
+ * @private
100
+ */
101
+ _normalizeJid(jid) {
102
+ if (!jid) return jid;
103
+
104
+ // Remove device ID (e.g., :0, :1, etc.) from the JID
105
+ // Pattern: number:deviceId@server becomes number@server
106
+ return jid.replace(/:\d+@/, '@');
107
+ }
108
+
109
+ async connect() {
110
+ try {
111
+
112
+ if (this._connectionState !== 'connecting') {
113
+ this._connectionState = 'connecting';
114
+ this.emit('connecting', 'Connecting to WhatsApp...');
115
+ }
116
+
117
+ const browserConfig = this.authmethod === 'pairing'
118
+ ? Browsers.windows('chrome')
119
+ : ["Innovators Soft", "chrome", "1000.26100.275.0"];
120
+
121
+ const { version: baileysVersion, isLatest: baileysIsLatest } = await fetchLatestBaileysVersion();
122
+ const { version: waWebVersion, isLatest: waWebIsLatest } = await fetchLatestWaWebVersion();
123
+
124
+ console.log('Using Baileys Version:', baileysVersion, baileysIsLatest ? ' isLatest true' : ' isLatest false');
125
+ console.log('Using WhatsApp Web Version:', waWebVersion, waWebIsLatest ? ' isLatest true' : ' isLatest false');
126
+
127
+ const { state, saveCreds } = await useMultiFileAuthState(this.sessionName)
128
+ const logger = P({ level: 'silent' })
129
+
130
+ this.sock = makeWASocket({
131
+ auth: state,
132
+ logger,
133
+ markOnlineOnConnect: false,
134
+ syncFullHistory: true,
135
+ generateHighQualityLinkPreview: true,
136
+ linkPreviewImageThumbnailWidth: 192,
137
+ emitOwnEvents: true,
138
+ browser: browserConfig,
139
+ version: waWebVersion,
140
+ cachedGroupMetadata: async (jid) => {
141
+ const cached = this.groupMetadataCache.get(jid);
142
+ if (cached) {
143
+ return cached;
144
+ }
145
+ try {
146
+ const metadata = await this.sock.groupMetadata(jid);
147
+ this.groupMetadataCache.set(jid, metadata);
148
+ return metadata;
149
+ } catch (error) {
150
+ console.error(`Error fetching metadata for group ${jid}:`, error);
151
+ return null;
152
+ }
153
+ }
154
+ });
155
+
156
+ this.store = this.sock.signalRepository.lidMapping;
157
+
158
+
159
+
160
+ this.sock.ev.on('connection.update', async (update) => {
161
+ const { connection, lastDisconnect, qr } = update;
162
+
163
+ if (qr && this.authmethod === 'qr') {
164
+ this.emit('qr', qr);
165
+ }
166
+ if (connection === 'open') {
167
+ if (this._connectionState !== 'connected') {
168
+ const user = getCurrentSenderInfo(this.sock.authState)
169
+ if (user) {
170
+ const userInfo = {
171
+ name: user.pushName || 'Unknown',
172
+ phone: user.phoneNumber,
173
+ platform: user.platform || 'Unknown',
174
+ isOnline: true
175
+ };
176
+ this.isConnected = true;
177
+ this._connectionState = 'connected';
178
+ this.emit('connected', userInfo);
179
+ }
180
+ }
181
+ }
182
+ else if (connection === 'close') {
183
+ const shouldReconnect = (lastDisconnect?.error instanceof Boom) ?
184
+ lastDisconnect.error?.output?.statusCode !== DisconnectReason.loggedOut : true;
185
+
186
+ if (this._connectionState !== 'disconnected') {
187
+ this.isConnected = false;
188
+ this._connectionState = 'disconnected';
189
+ this.emit('disconnected', lastDisconnect?.error);
190
+ }
191
+
192
+ if (shouldReconnect) {
193
+ this.connect();
194
+ } else if (lastDisconnect?.error?.output?.statusCode === DisconnectReason.loggedOut) {
195
+ await this.reinitialize();
196
+ }
197
+ }
198
+
199
+ // Handle pairing code request after connection is established but before login
200
+ if (this.authmethod === 'pairing' && connection === 'connecting' && !state.creds?.registered) {
201
+ const phoneNumber = this.pairingPhoneNumber;
202
+
203
+ if (phoneNumber) {
204
+ try {
205
+ // Wait a bit for the connection to initialize properly
206
+ setTimeout(async () => {
207
+ const customeCode = "INOVATOR";
208
+ try {
209
+ const code = await this.sock.requestPairingCode(phoneNumber, customeCode);
210
+ if (code) {
211
+ // Emit pairing code event so clients can handle it
212
+ this.emit('pairing-code', formatCode(code));
213
+ } else {
214
+ console.log("❌ Pairing code not found.");
215
+ }
216
+ } catch (error) {
217
+ console.error('Error requesting pairing code:', error);
218
+ this.emit('error', error);
219
+ }
220
+ }, 2000); // Wait 2 seconds before requesting pairing code
221
+ } catch (error) {
222
+ console.error('Error setting timeout for pairing code:', error);
223
+ this.emit('error', error);
224
+ }
225
+ }
226
+ }
227
+ })
228
+
229
+ /**
230
+ * Handle incoming messages
231
+ * @emits WhatsAppClient#message - When a new message is received
232
+ * @param {object} update - The message update object
233
+ */
234
+
235
+ this.sock.ev.on('messages.upsert', async (update) => {
236
+ // 📝 Store message for the Anti-Delete system
237
+ createMessageStoreHandler(this.messageStore)(update);
238
+
239
+ try {
240
+ if (update.type !== 'notify' || !update.messages?.length) return;
241
+ const [message] = update.messages;
242
+ if (!message || message.key?.fromMe) return;
243
+
244
+ // 🚫 Ignore all protocol messages (history sync, security notifications, app state sync, deleted messages, etc.)
245
+ if (message.message?.protocolMessage) return;
246
+
247
+ const msg = message.message || {};
248
+
249
+
250
+ let jid = message.key.remoteJid;
251
+
252
+ // Prefer PN (Phone Number) JID over LID for better compatibility
253
+ // remoteJidAlt contains the alternate JID (PN if primary is LID, or vice versa)
254
+ if (typeof message.key.remoteJidAlt === 'string' &&
255
+ message.key.remoteJidAlt.endsWith('@s.whatsapp.net')
256
+ ) {
257
+ jid = message.key.remoteJidAlt;
258
+ }
259
+ // Resolve the actual sender (preferring PN over LID)
260
+ const participant = message.key.participant || message.participant || null;
261
+ const participantAlt = message.key.participantAlt || null;
262
+
263
+ let sender = jid;
264
+ if (jid.endsWith('@g.us') || jid === 'status@broadcast') {
265
+ sender = (participantAlt && participantAlt.endsWith('@s.whatsapp.net'))
266
+ ? participantAlt
267
+ : (participant || jid);
268
+ }
269
+
270
+ // Resolve LID to PN for sender if needed
271
+ sender = await this._resolveLidToPn(sender);
272
+
273
+ const timestamp = new Date((message.messageTimestamp || Date.now()) * 1000);
274
+
275
+ const getText = () =>
276
+ msg.conversation ||
277
+ msg.extendedTextMessage?.text ||
278
+ msg.imageMessage?.caption ||
279
+ msg.videoMessage?.caption ||
280
+ '';
281
+
282
+ const getButtonText = () => {
283
+ if (msg.listResponseMessage)
284
+ return msg.listResponseMessage.title || msg.listResponseMessage.description || '';
285
+ if (msg.templateButtonReplyMessage)
286
+ return msg.templateButtonReplyMessage.selectedDisplayText || msg.templateButtonReplyMessage.selectedId || '';
287
+ if (msg.buttonsResponseMessage)
288
+ return msg.buttonsResponseMessage.selectedDisplayText || msg.buttonsResponseMessage.selectedButtonId || '';
289
+ if (msg.interactiveResponseMessage) {
290
+ const i = msg.interactiveResponseMessage;
291
+ return i.listResponse?.title ||
292
+ i.listResponse?.description ||
293
+ i.nativeFlowResponse?.response?.reply ||
294
+ i.reply ||
295
+ i.buttonReplyMessage?.displayText || '';
296
+ }
297
+ return '';
298
+ };
299
+
300
+ const reply = async (text) => this.sock.sendMessage(jid, { text }, { quoted: message, ai: true });
301
+
302
+ // ✅ Handle status updates (stories)
303
+ if (jid === 'status@broadcast') {
304
+ this.emit('status', {
305
+ from: sender,
306
+ sender,
307
+ participant,
308
+ participantAlt,
309
+ body: getText(),
310
+ hasMedia: Boolean(msg.imageMessage || msg.videoMessage),
311
+ timestamp,
312
+ key: message.key,
313
+ raw: message,
314
+ // Reply to status
315
+ reply: async (text) => {
316
+ if (!sender) throw new Error('Missing participant JID');
317
+ return this.sock.sendMessage(sender, { text }, { quoted: message, ai: true });
318
+ },
319
+ // 👍 Like (react) to status
320
+ like: async (emoji = '❤️') => {
321
+ if (!sender) throw new Error('Missing participant JID');
322
+
323
+ // Read the message first
324
+ try {
325
+ await this.sock.readMessages([message.key]);
326
+ } catch (readError) {
327
+ console.error('Error reading status message:', readError);
328
+ }
329
+
330
+ // Then send the reaction
331
+ return this.sock.sendMessage(sender, {
332
+ react: {
333
+ text: emoji,
334
+ key: message.key
335
+ }
336
+ }, { ai: true });
337
+ }
338
+ });
339
+ return;
340
+ }
341
+
342
+ // ✅ Handle normal/chat messages
343
+ const buttonText = getButtonText();
344
+ const body = buttonText || getText() ||
345
+ msg.listResponseMessage?.singleSelectReply?.selectedRowId || '';
346
+
347
+ this.emit('message', {
348
+ from: jid,
349
+ sender,
350
+ participant,
351
+ participantAlt,
352
+ body,
353
+ hasMedia: Boolean(msg.imageMessage || msg.videoMessage || msg.audioMessage || msg.documentMessage),
354
+ isGroup: jid.endsWith('@g.us'),
355
+ timestamp,
356
+ isButtonResponse: Boolean(buttonText),
357
+ buttonId: msg?.listResponseMessage?.singleSelectReply?.selectedRowId ||
358
+ msg?.templateButtonReplyMessage?.selectedId ||
359
+ msg?.buttonsResponseMessage?.selectedButtonId || null,
360
+ buttonText,
361
+ raw: message,
362
+ reply,
363
+ });
364
+ } catch (err) {
365
+ console.error('Error processing message:', err);
366
+ }
367
+ });
368
+
369
+ // 🛡️ Anti-Delete System: Handle message revokes/deletions
370
+ const antiDeleteHandler = createAntiDeleteHandler(this.messageStore);
371
+
372
+ this.sock.ev.on('messages.update', (updates) => {
373
+ const deletedMessages = antiDeleteHandler(updates);
374
+ for (const info of deletedMessages) {
375
+ let jid = info.key.remoteJid;
376
+ if (typeof info.key.remoteJidAlt === 'string' && info.key.remoteJidAlt.endsWith('@s.whatsapp.net')) {
377
+ jid = info.key.remoteJidAlt;
378
+ }
379
+
380
+ this.emit('message-deleted', {
381
+ jid: jid,
382
+ originalMessage: info.originalMessage,
383
+ key: info.key
384
+ });
385
+ }
386
+ });
387
+
388
+ // 👍 Handle message reactions
389
+ this.sock.ev.on('messages.reaction', async (reactions) => {
390
+ try {
391
+ for (const reaction of reactions) {
392
+ // Get the chat JID, preferring PN over LID
393
+ let jid = reaction.key.remoteJid;
394
+ if (typeof reaction.key.remoteJidAlt === 'string' &&
395
+ reaction.key.remoteJidAlt.endsWith('@s.whatsapp.net')) {
396
+ jid = reaction.key.remoteJidAlt;
397
+ }
398
+
399
+ // Resolve the sender (who reacted), preferring PN over LID
400
+ const participant = reaction.key.participant || null;
401
+ const participantAlt = reaction.key.participantAlt || null;
402
+
403
+ let sender = jid;
404
+ if (jid.endsWith('@g.us') || jid === 'status@broadcast') {
405
+ sender = (participantAlt && participantAlt.endsWith('@s.whatsapp.net'))
406
+ ? participantAlt
407
+ : (participant || jid);
408
+ }
409
+
410
+ // Resolve LID to PN for sender if needed
411
+ sender = await this._resolveLidToPn(sender);
412
+
413
+ // Emit the reaction event
414
+ this.emit('message-reaction', {
415
+ from: jid,
416
+ sender: sender,
417
+ participant: participant,
418
+ participantAlt: participantAlt,
419
+ emoji: reaction.reaction?.text || null,
420
+ isRemoved: !reaction.reaction?.text,
421
+ messageKey: reaction.key,
422
+ timestamp: new Date(),
423
+ raw: reaction
424
+ });
425
+ }
426
+ } catch (error) {
427
+ console.error('Error processing message reaction:', error);
428
+ }
429
+ });
430
+
431
+ // Handle incoming calls
432
+ this.sock.ev.on('call', async (call) => {
433
+ try {
434
+ // Extract phone number from LID if available
435
+ for (const callData of call) {
436
+ if (callData.chatId || callData.from) {
437
+ const jid = callData.chatId || callData.from;
438
+
439
+ // Resolve LID to PN using the helper method
440
+ const resolvedJid = await this._resolveLidToPn(jid);
441
+ callData.phoneNumber = resolvedJid.split(':')[0].split('@')[0];
442
+ }
443
+ }
444
+
445
+ await this.emit('call', call);
446
+ } catch (error) {
447
+ console.error('Error in call handler:', error);
448
+ this.emit('error', error);
449
+ }
450
+ });
451
+
452
+ // Handle LID/PN mapping updates
453
+ this.sock.ev.on('lid-mapping.update', async (update) => {
454
+ try {
455
+ // Store the mapping for future use
456
+ if (update && Object.keys(update).length > 0) {
457
+ // The update object contains PN -> LID mappings
458
+ // They are automatically stored in sock.signalRepository.lidMapping
459
+ this.emit('lid-mapping-update', update);
460
+ }
461
+ } catch (error) {
462
+ console.error('Error processing LID mapping update:', error);
463
+ }
464
+ });
465
+
466
+ // Handle credential updates
467
+ this.sock.ev.on('creds.update', saveCreds);
468
+
469
+ // Handle group events to keep the cache updated
470
+ this.sock.ev.on('groups.upsert', (groups) => {
471
+ for (const group of groups) {
472
+ this.updateGroupMetadataCache(group.id, group);
473
+ }
474
+ });
475
+
476
+ this.sock.ev.on('groups.update', (groups) => {
477
+ for (const group of groups) {
478
+ // Get existing cache and update it with new information
479
+ const cached = this.groupMetadataCache.get(group.id);
480
+ if (cached) {
481
+ // Merge the updated fields with existing cached data
482
+ const updated = { ...cached, ...group };
483
+ this.updateGroupMetadataCache(group.id, updated);
484
+ }
485
+ }
486
+ });
487
+
488
+ this.sock.ev.on('group-participants.update', async (update) => {
489
+ // When participants change, refresh the group metadata
490
+ try {
491
+ const metadata = await this.sock.groupMetadata(update.id);
492
+ this.updateGroupMetadataCache(update.id, metadata);
493
+ } catch (error) {
494
+ // Check if group no longer exists or bot was removed (404 item-not-found)
495
+ const isNotFound = error.data === 404 ||
496
+ error.message?.includes('item-not-found') ||
497
+ error.output?.statusCode === 404;
498
+
499
+ if (isNotFound) {
500
+ // Group no longer exists or bot was removed - clean up cache
501
+ this.clearGroupMetadataCache(update.id);
502
+ this.emit('group-left', {
503
+ id: update.id,
504
+ reason: 'Group not found or bot was removed'
505
+ });
506
+ } else {
507
+ console.error(`Error refreshing metadata for group ${update.id}:`, error);
508
+ }
509
+ }
510
+ });
511
+
512
+ } catch (error) {
513
+ console.error('Error in connect:', error);
514
+ this.emit('error', error);
515
+ throw error;
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Send a message to a chat
521
+ * @param {string} chatId - The ID of the chat to send the message to
522
+ * @param {string|object} message - The message content (string) or message object
523
+ * @param {object} options - Additional options for sending the message
524
+ * @returns {Promise<object>} The sent message info
525
+ * @throws {Error} If client is not connected or message sending fails
526
+ */
527
+ async sendMessage(chatId, message, options = {}) {
528
+ if (!this.isConnected) {
529
+ throw new Error('Client is not connected');
530
+ }
531
+
532
+ let messageContent = {};
533
+
534
+ // Handle different message types
535
+ if (typeof message === 'string') {
536
+ messageContent = { text: message };
537
+ } else if (message && typeof message === 'object') {
538
+ // Handle different message types
539
+ switch (message.type) {
540
+ case 'text':
541
+ messageContent = { text: message.text };
542
+ if (message.mentions) {
543
+ messageContent.mentions = message.mentions;
544
+ }
545
+ break;
546
+
547
+ case 'location':
548
+ messageContent = {
549
+ location: {
550
+ degreesLatitude: message.latitude,
551
+ degreesLongitude: message.longitude,
552
+ name: message.name,
553
+ address: message.address
554
+ }
555
+ };
556
+ break;
557
+
558
+ case 'contact':
559
+ messageContent = {
560
+ contacts: {
561
+ displayName: message.fullName,
562
+ contacts: [{
563
+ displayName: message.fullName,
564
+ vcard: `BEGIN:VCARD\nVERSION:3.0\n` +
565
+ `FN:${message.fullName}\n` +
566
+ (message.organization ? `ORG:${message.organization};\n` : '') +
567
+ (message.phoneNumber ? `TEL;type=CELL;type=VOICE;waid=${message.phoneNumber}:+${message.phoneNumber}\n` : '') +
568
+ 'END:VCARD'
569
+ }]
570
+ }
571
+ };
572
+ break;
573
+
574
+ case 'reaction':
575
+ messageContent = {
576
+ react: {
577
+ text: message.emoji,
578
+ key: message.messageKey || message.message?.key || message.key
579
+ }
580
+ };
581
+ break;
582
+
583
+ default:
584
+ throw new Error('Invalid message type');
585
+ }
586
+ } else {
587
+ throw new Error('Invalid message content');
588
+ }
589
+
590
+ try {
591
+ return await this.sock.sendMessage(chatId, messageContent, { ...options, ai: true });
592
+ } catch (error) {
593
+ console.error('Error sending message:', error);
594
+ throw error;
595
+ }
596
+ }
597
+ /**
598
+ * Send a media file to a chat
599
+ * @param {string} chatId - The ID of the chat to send the media to
600
+ * @param {string} filePath - Path to the media file
601
+ * @param {object} options - Additional options for the media message
602
+ * @returns {Promise<object>} The sent message info
603
+ * @throws {Error} If client is not connected or file not found
604
+ */
605
+ async sendMedia(chatId, filePath, options = {}) {
606
+ if (!this.isConnected) {
607
+ throw new Error('Client is not connected');
608
+ }
609
+
610
+ try {
611
+ // Check if file exists
612
+ if (!fs.existsSync(filePath)) {
613
+ throw new Error('File not found: ' + filePath);
614
+ }
615
+
616
+ const fileExtension = path.extname(filePath).toLowerCase();
617
+ const caption = options.caption || '';
618
+ let mediaMessage = {};
619
+
620
+ // Handle different media types
621
+ switch (fileExtension) {
622
+ case '.gif':
623
+ case '.mp4':
624
+ mediaMessage = {
625
+ video: fs.readFileSync(filePath),
626
+ caption: caption,
627
+ gifPlayback: options.asGif || fileExtension === '.gif',
628
+ }
629
+ break;
630
+
631
+ // Handle audio files
632
+ case '.mp3':
633
+ case '.ogg':
634
+ case '.wav':
635
+ mediaMessage = {
636
+ audio: {
637
+ url: filePath
638
+ },
639
+ mimetype: 'audio/mp4',
640
+ };
641
+ break;
642
+
643
+ // Handle image files
644
+ case '.jpg':
645
+ case '.jpeg':
646
+ case '.png':
647
+ mediaMessage = {
648
+ image: fs.readFileSync(filePath),
649
+ caption: caption,
650
+ };
651
+ break;
652
+
653
+ default:
654
+ throw new Error('Unsupported file type: ' + fileExtension);
655
+ }
656
+
657
+ return await this.sock.sendMessage(chatId, mediaMessage, { ai: true });
658
+ } catch (error) {
659
+ console.error('Error sending media:', error);
660
+ throw error;
661
+ }
662
+ }
663
+
664
+ /**
665
+ * Send a document to a chat
666
+ * @param {string} chatId - The ID of the chat to send the document to
667
+ * @param {string} filePath - Path to the document file
668
+ * @param {string} [caption=''] - Optional caption for the document
669
+ * @returns {Promise<object>} The sent message info
670
+ * @throws {Error} If client is not connected or file not found
671
+ */
672
+ async sendDocument(chatId, filePath, caption = '') {
673
+ if (!this.isConnected) {
674
+ throw new Error('Client is not connected');
675
+ }
676
+
677
+ try {
678
+ // Check if file exists
679
+ if (!fs.existsSync(filePath)) {
680
+ throw new Error('File not found: ' + filePath);
681
+ }
682
+
683
+ const fileBuffer = fs.readFileSync(filePath);
684
+ const fileName = path.basename(filePath);
685
+ const mimeType = mime.getType(filePath);
686
+
687
+ return await this.sock.sendMessage(chatId, {
688
+ document: fileBuffer,
689
+ caption: caption,
690
+ mimetype: mimeType,
691
+ fileName: fileName,
692
+ }, { ai: true });
693
+ } catch (error) {
694
+ console.error('Error sending document:', error);
695
+ throw error;
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Send a message with interactive buttons
701
+ * @param {string} chatId - The ID of the chat to send the message to
702
+ * @param {object} options - Options for the button message
703
+ * @param {string} [options.text] - The text content of the message
704
+ * @param {string} [options.imagePath] - Optional path to an image to include
705
+ * @param {string} [options.caption] - Caption for the image
706
+ * @param {string} [options.title] - Title for the message
707
+ * @param {string} [options.footer] - Footer text for the message
708
+ * @param {Array} [options.interactiveButtons=[]] - Array of button objects
709
+ * @param {boolean} [options.hasMediaAttachment=false] - Whether the message has a media attachment
710
+ * @param {object} [extraOptions={}] - Additional options for the message
711
+ * @returns {Promise<object>} The sent message info
712
+ * @throws {Error} If client is not connected or message sending fails
713
+ */
714
+ async sendButtons(chatId, options = {}, extraOptions = {}) {
715
+ if (!this.isConnected) {
716
+ throw new Error('Client is not connected');
717
+ }
718
+
719
+ const {
720
+ text,
721
+ imagePath,
722
+ caption,
723
+ title,
724
+ footer,
725
+ interactiveButtons = [],
726
+ hasMediaAttachment = false,
727
+ } = options;
728
+
729
+ let messageContent = {};
730
+
731
+ try {
732
+ if (imagePath) {
733
+ // Handle message with image
734
+ const imageBuffer = fs.readFileSync(imagePath);
735
+ messageContent = {
736
+ image: imageBuffer,
737
+ caption: caption,
738
+ title: title,
739
+ footer: footer,
740
+ interactiveButtons: interactiveButtons,
741
+ hasMediaAttachment: hasMediaAttachment,
742
+ };
743
+ } else {
744
+ // Handle text-only message
745
+ messageContent = {
746
+ text: text,
747
+ title: title,
748
+ footer: footer,
749
+ interactiveButtons: interactiveButtons,
750
+ };
751
+ }
752
+
753
+ // Send the message with buttons
754
+ return await this.sock.sendMessage(chatId, messageContent, { ...extraOptions, ai: true });
755
+ } catch (error) {
756
+ console.error('Error sending buttons:', error);
757
+ throw error;
758
+ }
759
+ }
760
+
761
+ /**
762
+ * Send an interactive list message
763
+ * @param {string} chatId - The ID of the chat to send the list to
764
+ * @param {object} listOptions - Options for the list message
765
+ * @param {string} listOptions.text - The text content of the message
766
+ * @param {string} listOptions.title - Title of the list
767
+ * @param {string} [listOptions.footer=''] - Optional footer text
768
+ * @param {string} [listOptions.buttonText='Tap here'] - Text for the button
769
+ * @param {Array<object>} listOptions.sections - Array of section objects
770
+ * @returns {Promise<object>} The sent message info
771
+ * @throws {Error} If client is not connected or message sending fails
772
+ */
773
+ async SendList(chatId, listOptions) {
774
+ if (!this.isConnected) {
775
+ throw new Error('Client is not connected');
776
+ }
777
+
778
+ try {
779
+ const listMessage = {
780
+ text: listOptions.text,
781
+ title: listOptions.title,
782
+ footer: listOptions.footer || '',
783
+ buttonText: listOptions.buttonText || 'Tap here',
784
+ sections: listOptions.sections.map((section) => ({
785
+ title: section.title,
786
+ rows: section.rows.map((row) => ({
787
+ title: row.title,
788
+ rowId: row.id,
789
+ description: row.description,
790
+ })),
791
+ })),
792
+ };
793
+
794
+ return await this.sock.sendMessage(chatId, listMessage, { ai: true });
795
+ } catch (error) {
796
+ console.error('Error sending list message:', error);
797
+ throw error;
798
+ }
799
+ }
800
+ /**
801
+ * Send an external ad reply with a local image
802
+ * @param {string} number - The phone number to send the ad to
803
+ * @param {string} localImagePath - Path to the local image file
804
+ * @param {string} title - Title of the ad
805
+ * @param {string} body - Body of the ad
806
+ * @returns {Promise<void>}
807
+ * @throws {Error} If client is not connected or message sending fails
808
+ */
809
+
810
+ async sendAdReply(number, msg, imgpath, title, body, sourceurl) {
811
+ if (!this.isConnected) {
812
+ throw new Error('Client is not connected');
813
+ }
814
+
815
+ try {
816
+
817
+ // Read the local image as Buffer
818
+ const bufferLocalFile = fs.readFileSync(imgpath);
819
+
820
+ // Send message with external ad reply using local image
821
+ await this.sock.sendMessage(number, {
822
+ text: msg,
823
+ contextInfo: {
824
+ externalAdReply: {
825
+ title: title || 'Ad Title',
826
+ body: body || 'Ad Description',
827
+ mediaType: 1, // Image
828
+ previewType: 0,
829
+ showAdAttribution: true,
830
+ renderLargerThumbnail: true,
831
+ thumbnail: bufferLocalFile,
832
+ sourceUrl: sourceurl || 'https://m.facebook.com/innovatorssoft',
833
+ mediaUrl: sourceurl || 'https://m.facebook.com/innovatorssoft'
834
+ }
835
+ }
836
+ }, { ai: true });
837
+
838
+ } catch (err) {
839
+ console.error('Failed to send externalAdReply:', err);
840
+ }
841
+ }
842
+
843
+ /**
844
+ * Get all groups the bot is a member of
845
+ * @returns {Promise<Array<Group>>} Array of Group instances
846
+ * @throws {Error} If client is not connected or an error occurs
847
+ */
848
+ async getAllGroups() {
849
+ if (!this.isConnected) {
850
+ throw new Error('Client is not connected');
851
+ }
852
+
853
+ try {
854
+ const groupsData = await this.sock.groupFetchAllParticipating();
855
+ return Object.values(groupsData).map(
856
+ (groupData) => new Group(this, groupData)
857
+ );
858
+ } catch (error) {
859
+ console.error('Error fetching groups:', error);
860
+ throw error;
861
+ }
862
+ }
863
+
864
+ /**
865
+ * Add or remove participants from a group
866
+ * @param {string} groupId - The ID of the group
867
+ * @param {Array<string>} participantIds - Array of participant IDs to modify
868
+ * @param {string} action - Action to perform ('add', 'remove', 'promote', 'demote')
869
+ * @returns {Promise<Array>} Array of results for each participant update
870
+ * @throws {Error} If client is not connected or an error occurs
871
+ */
872
+ async changeGroupParticipants(groupId, participantIds, action) {
873
+ if (!this.isConnected) {
874
+ throw new Error('Client is not connected');
875
+ }
876
+
877
+ const results = [];
878
+
879
+ try {
880
+ for (const participantId of participantIds) {
881
+ try {
882
+ let updateResult = await this.sock.groupParticipantsUpdate(
883
+ groupId,
884
+ [participantId],
885
+ action
886
+ );
887
+
888
+ // Baileys may return various formats:
889
+ // 1. Array: [{ status: 200, jid: '...' }]
890
+ // 2. Array with nested: [{ '123@s.whatsapp.net': { code: 200 } }]
891
+ // 3. Direct object: { status: 200, jid: '...' }
892
+ let participantResult = Array.isArray(updateResult) ? updateResult[0] : updateResult;
893
+
894
+ // Handle the { [jid]: { code: ... } } format
895
+ if (participantResult && typeof participantResult === 'object') {
896
+ // Check if the result has the participant JID as a key
897
+ const jidKey = Object.keys(participantResult).find(k => k.includes('@'));
898
+ if (jidKey && participantResult[jidKey]) {
899
+ const innerResult = participantResult[jidKey];
900
+ participantResult = {
901
+ status: innerResult.code || innerResult.status || 200,
902
+ jid: jidKey,
903
+ message: innerResult.message || (innerResult.code == 200 ? 'Success' : undefined),
904
+ ...innerResult
905
+ };
906
+ }
907
+ }
908
+
909
+ // Normalize status to number
910
+ let status = participantResult?.status || participantResult?.code;
911
+
912
+ // If no status but no error, assume success
913
+ if (status === undefined && !participantResult?.error) {
914
+ status = 200;
915
+ }
916
+
917
+ if (action === 'add' && (status === 403 || status === '403')) {
918
+ try {
919
+ await this.sendGroupInvitation(groupId, participantId, null);
920
+ participantResult.invitationSent = true;
921
+ participantResult.message = 'Invitation link sent due to privacy settings';
922
+ participantResult.status = 403;
923
+ } catch (invError) {
924
+ participantResult.invitationSent = false;
925
+ participantResult.error = 'Privacy restricted, and failed to send invitation link';
926
+ participantResult.status = 403;
927
+ }
928
+ } else if (participantResult) {
929
+ participantResult.status = status ? parseInt(status) : 200;
930
+ if (participantResult.status === 200) {
931
+ participantResult.message = participantResult.message || 'Successfully added to group';
932
+ }
933
+ }
934
+
935
+ results.push(participantResult || { status: 200, jid: participantId, message: 'Added successfully' });
936
+ } catch (participantError) {
937
+
938
+ const statusCode = participantError.output?.statusCode || participantError.data?.status;
939
+
940
+ if (action === 'add' && (statusCode === 403 || statusCode === '403')) {
941
+ try {
942
+ await this.sendGroupInvitation(groupId, participantId, null);
943
+ results.push({
944
+ status: 403,
945
+ jid: participantId,
946
+ invitationSent: true,
947
+ message: 'Invitation link sent due to privacy settings'
948
+ });
949
+ continue;
950
+ } catch (invError) {
951
+ console.error('Failed to send fallback invitation:', invError);
952
+ }
953
+ }
954
+
955
+ results.push({
956
+ status: statusCode || 500,
957
+ jid: participantId,
958
+ error: participantError.message || 'Unknown error'
959
+ });
960
+ }
961
+ }
962
+ return results;
963
+ } catch (error) {
964
+ throw error;
965
+ }
966
+ }
967
+
968
+ /**
969
+ * Get group name from metadata
970
+ * @param {string} groupId - The group JID
971
+ * @returns {Promise<string>} The group name
972
+ * @private
973
+ */
974
+ async _getGroupName(groupId) {
975
+ try {
976
+ const metadata = await this.getGroupMetadata(groupId);
977
+ return metadata.subject || 'Group';
978
+ } catch (error) {
979
+ console.error('Error getting group name:', error);
980
+ return 'Group';
981
+ }
982
+ }
983
+
984
+ /**
985
+ * Send a group invitation link to a user
986
+ * @param {string} groupId - The group JID
987
+ * @param {string} participantId - The participant JID to send invitation to
988
+ * @param {string} [customMessage] - Optional custom invitation message
989
+ * @returns {Promise<object>} The sent message info
990
+ * @throws {Error} If client is not connected or an error occurs
991
+ */
992
+
993
+ async sendGroupInvitation(groupId, participantId, customMessage) {
994
+ if (!this.isConnected) {
995
+ throw new Error('Client is not connected');
996
+ }
997
+
998
+ try {
999
+ // Get group invite code
1000
+ const inviteCode = await this.sock.groupInviteCode(groupId);
1001
+ const groupName = await this._getGroupName(groupId);
1002
+
1003
+ // Create the invitation link
1004
+ const inviteLink = `https://chat.whatsapp.com/${inviteCode}`;
1005
+
1006
+ // Create the invitation message
1007
+ const message = customMessage
1008
+ ? `${customMessage}\n\n${inviteLink}`
1009
+ : `📨 *Group Invitation*\n\n` +
1010
+ `You have been invited to join:\n` +
1011
+ `*${groupName}*\n\n` +
1012
+ `Click the link below to join:\n${inviteLink}`;
1013
+
1014
+ // Send as text message
1015
+ return await this.sock.sendMessage(participantId, {
1016
+ text: message
1017
+ }, { ai: true });
1018
+ } catch (error) {
1019
+ console.error('Error sending group invitation:', error);
1020
+ throw error;
1021
+ }
1022
+ }
1023
+
1024
+ /**
1025
+ * Mark a message as read
1026
+ * @param {object|string} messageKey - The message key object or message ID
1027
+ * @returns {Promise<void>}
1028
+ * @throws {Error} If client is not connected or an error occurs
1029
+ */
1030
+ async readMessage(messageKey) {
1031
+ if (!this.isConnected) {
1032
+ throw new Error('Client is not connected');
1033
+ }
1034
+
1035
+ try {
1036
+ // If messageKey is a string (legacy), convert to key object format
1037
+ // Otherwise use it directly as a key object
1038
+ if (typeof messageKey === 'string') {
1039
+ // Legacy support: just ID string
1040
+ await this.sock.readMessages([{ id: messageKey }]);
1041
+ } else {
1042
+ // Proper key object with remoteJid, id, fromMe, etc.
1043
+ await this.sock.readMessages([messageKey]);
1044
+ }
1045
+ } catch (error) {
1046
+ console.error('Error marking message as read:', error);
1047
+ throw error;
1048
+ }
1049
+ }
1050
+
1051
+ /**
1052
+ * Check if a phone number is registered on WhatsApp
1053
+ * @param {string} phoneNumber - The phone number to check (with country code, without '+')
1054
+ * @returns {Promise<boolean>} True if the number is on WhatsApp, false otherwise
1055
+ * @throws {Error} If an error occurs during the check
1056
+ */
1057
+ async isNumberOnWhatsApp(phoneNumber) {
1058
+ if (!this.isConnected) {
1059
+ throw new Error('Client is not connected');
1060
+ }
1061
+
1062
+ try {
1063
+ const result = await this.sock.onWhatsApp(phoneNumber);
1064
+ return result.length > 0 && result[0].exists === true;
1065
+ } catch (error) {
1066
+ console.error('Error checking if number is on WhatsApp:', error);
1067
+ throw error;
1068
+ }
1069
+ }
1070
+
1071
+ /**
1072
+ * Get the profile picture URL for a contact or group
1073
+ * @param {string} id - The contact or group JID
1074
+ * @returns {Promise<string|undefined>} The profile picture URL, or undefined if not available
1075
+ */
1076
+ async getProfilePicture(id) {
1077
+ if (!this.isConnected) {
1078
+ throw new Error('Client is not connected');
1079
+ }
1080
+
1081
+ try {
1082
+ return await this.sock.profilePictureUrl(id);
1083
+ } catch (error) {
1084
+ // If the error is because the user has no profile picture, return undefined
1085
+ const isStatus = (err, code) => (
1086
+ (err && typeof err.message === 'string' && err.message.includes(String(code))) ||
1087
+ (err && (err.status === code || err.code === code || err.data === code)) ||
1088
+ (err && err.response && err.response.status === code) ||
1089
+ (err && err.output && err.output.statusCode === code)
1090
+ );
1091
+ if (isStatus(error, 404) || isStatus(error, 401)) {
1092
+ return undefined;
1093
+ }
1094
+ console.error('Error getting profile picture:', error);
1095
+ throw error;
1096
+ }
1097
+ }
1098
+ /**
1099
+ * Reject an incoming call
1100
+ * @param {string} callId - The ID of the call to reject
1101
+ * @param {object} callInfo - Additional call information
1102
+ * @returns {Promise<void>}
1103
+ */
1104
+ async rejectCall(callId, callInfo) {
1105
+ if (!this.isConnected) {
1106
+ throw new Error('Client is not connected');
1107
+ }
1108
+
1109
+ try {
1110
+ await this.sock.rejectCall(callId, callInfo);
1111
+ } catch (error) {
1112
+ console.error('Error rejecting call:', error);
1113
+ throw error;
1114
+ }
1115
+ }
1116
+
1117
+ /**
1118
+ * Get the underlying socket instance
1119
+ * @returns {object} The socket instance
1120
+ */
1121
+ getSock() {
1122
+ return this.sock;
1123
+ }
1124
+
1125
+ /**
1126
+ * Send typing indicator to a chat
1127
+ * @param {string} jid - The JID of the chat
1128
+ * @returns {Promise<void>}
1129
+ */
1130
+ async sendStateTyping(jid) {
1131
+ if (!this.sock) throw new Error('Not connected to WhatsApp');
1132
+ await this.sock.sendPresenceUpdate("composing", jid);
1133
+ }
1134
+
1135
+ /**
1136
+ * Send recording indicator to a chat
1137
+ * @param {string} jid - The JID of the chat
1138
+ * @returns {Promise<void>}
1139
+ */
1140
+ async sendStateRecording(jid) {
1141
+ if (!this.sock) throw new Error('Not connected to WhatsApp');
1142
+ await this.sock.sendPresenceUpdate("recording", jid);
1143
+ }
1144
+
1145
+ /**
1146
+ * Clear typing/recording indicator from a chat
1147
+ * @param {string} jid - The JID of the chat
1148
+ * @returns {Promise<void>}
1149
+ */
1150
+ async clearState(jid) {
1151
+ if (!this.sock) throw new Error('Not connected to WhatsApp');
1152
+ await this.sock.sendPresenceUpdate("paused", jid);
1153
+ }
1154
+
1155
+ /**
1156
+ * Reinitialize the WhatsApp client by clearing the session and reconnecting
1157
+ * @returns {Promise<void>}
1158
+ */
1159
+ async reinitialize() {
1160
+ try {
1161
+ //console.log('Starting session reinitialization...');
1162
+
1163
+ // Reset connection state
1164
+ this.isConnected = false;
1165
+
1166
+ // Clear the session data if it exists
1167
+ if (fs.existsSync(this.sessionName)) {
1168
+ await fs.promises.rm(this.sessionName, {
1169
+ recursive: true,
1170
+ force: true,
1171
+ });
1172
+ //console.log('Cleared existing session data');
1173
+ }
1174
+
1175
+ // Clear any existing socket
1176
+ if (this.sock) {
1177
+ try {
1178
+ this.sock.ev.removeAllListeners();
1179
+ this.sock.end(undefined);
1180
+ } catch (e) {
1181
+ console.error('Error while cleaning up socket:', e);
1182
+ }
1183
+ this.sock = null;
1184
+ }
1185
+
1186
+ // Add a small delay before reconnecting
1187
+ await new Promise(resolve => setTimeout(resolve, 1000));
1188
+
1189
+ // Reconnect with a fresh session
1190
+ //console.log('Establishing new connection...');
1191
+ await this.connect(1); // Start with attempt 1
1192
+ this.emit('reinitialized');
1193
+ } catch (error) {
1194
+ console.error('Error during reinitialization:', error);
1195
+ this.emit('error', error);
1196
+ throw error;
1197
+ }
1198
+ }
1199
+ /**
1200
+ * Get LID (Local Identifier) for a phone number
1201
+ * @param {string} phoneNumber - Phone number in format: '1234567890@s.whatsapp.net'
1202
+ * @returns {Promise<string|undefined>} The LID for the phone number, or undefined if not found
1203
+ */
1204
+ async getLIDForPN(phoneNumber) {
1205
+ if (!this.sock || !this.store) {
1206
+ throw new Error('Client is not connected');
1207
+ }
1208
+ try {
1209
+ return await this.store.getLIDForPN(phoneNumber);
1210
+ } catch (error) {
1211
+ console.error('Error getting LID for PN:', error);
1212
+ return undefined;
1213
+ }
1214
+ }
1215
+
1216
+ /**
1217
+ * Get Phone Number for a LID (Local Identifier)
1218
+ * @param {string} lid - LID in format: '123456@lid'
1219
+ * @returns {Promise<string|undefined>} The phone number for the LID, or undefined if not found
1220
+ */
1221
+ async getPNForLID(lid) {
1222
+ if (!this.sock || !this.store) {
1223
+ throw new Error('Client is not connected');
1224
+ }
1225
+ try {
1226
+ return await this.store.getPNForLID(lid);
1227
+ } catch (error) {
1228
+ console.error('Error getting PN for LID:', error);
1229
+ return undefined;
1230
+ }
1231
+ }
1232
+
1233
+ /**
1234
+ * Get multiple LIDs for multiple phone numbers
1235
+ * @param {Array<string>} phoneNumbers - Array of phone numbers
1236
+ * @returns {Promise<Array<string>>} Array of LIDs
1237
+ */
1238
+ async getLIDsForPNs(phoneNumbers) {
1239
+ if (!this.sock || !this.store) {
1240
+ throw new Error('Client is not connected');
1241
+ }
1242
+ try {
1243
+ return await this.store.getLIDsForPNs(phoneNumbers);
1244
+ } catch (error) {
1245
+ console.error('Error getting LIDs for PNs:', error);
1246
+ return [];
1247
+ }
1248
+ }
1249
+
1250
+ /**
1251
+ * Get group metadata with caching
1252
+ * @param {string} jid - The group JID
1253
+ * @returns {Promise<object>} The group metadata
1254
+ */
1255
+ async getGroupMetadata(jid) {
1256
+ if (!this.isConnected) {
1257
+ throw new Error('Client is not connected');
1258
+ }
1259
+
1260
+ // Check cache first
1261
+ const cached = this.groupMetadataCache.get(jid);
1262
+ if (cached) {
1263
+ return cached;
1264
+ }
1265
+
1266
+ // Fetch from WhatsApp if not cached
1267
+ try {
1268
+ const metadata = await this.sock.groupMetadata(jid);
1269
+ // Cache the result
1270
+ this.groupMetadataCache.set(jid, metadata);
1271
+ return metadata;
1272
+ } catch (error) {
1273
+ console.error(`Error fetching metadata for group ${jid}:`, error);
1274
+ throw error;
1275
+ }
1276
+ }
1277
+
1278
+ /**
1279
+ * Update group metadata in cache
1280
+ * @param {string} jid - The group JID
1281
+ * @param {object} metadata - The group metadata
1282
+ */
1283
+ updateGroupMetadataCache(jid, metadata) {
1284
+ if (this.groupMetadataCache) {
1285
+ this.groupMetadataCache.set(jid, metadata);
1286
+ }
1287
+ }
1288
+
1289
+ /**
1290
+ * Clear group metadata from cache
1291
+ * @param {string} jid - The group JID
1292
+ */
1293
+ clearGroupMetadataCache(jid) {
1294
+ if (this.groupMetadataCache) {
1295
+ this.groupMetadataCache.del(jid);
1296
+ }
1297
+ }
1298
+
1299
+ /**
1300
+ * Clear all group metadata from cache
1301
+ */
1302
+ clearAllGroupMetadataCache() {
1303
+ if (this.groupMetadataCache) {
1304
+ this.groupMetadataCache.flushAll();
1305
+ }
1306
+ }
1307
+
1308
+ /**
1309
+ * Log out from WhatsApp
1310
+ * @returns {Promise<boolean>} True if logout was successful
1311
+ * @throws {Error} If logout fails
1312
+ */
1313
+ async logout() {
1314
+ if (!this.sock) {
1315
+ this.emit('logout', 'Already logged out');
1316
+ return true;
1317
+ }
1318
+
1319
+ try {
1320
+ // Properly close the socket connection
1321
+ await this.sock.logout();
1322
+ await this.sock.end();
1323
+ this.sock = null;
1324
+
1325
+ // Remove session data if it exists
1326
+ if (fs.existsSync(this.sessionName)) {
1327
+ fs.rmSync(this.sessionName, {
1328
+ recursive: true,
1329
+ force: true,
1330
+ });
1331
+ }
1332
+
1333
+ // Update connection state and emit event
1334
+ this.isConnected = false;
1335
+ this.emit('logout', 'Logged out successfully');
1336
+
1337
+ return true;
1338
+ } catch (error) {
1339
+ console.error('Logout error:', error);
1340
+ this.emit('error', new Error(`Failed to logout: ${error.message}`));
1341
+ throw error;
1342
+ }
1343
+ }
1344
+
1345
+ /**
1346
+ * Download media from a message
1347
+ * @param {object} message - The message object containing media (must have raw property)
1348
+ * @returns {Promise<object|null>} Object with buffer, mimetype, and extension, or null if no media
1349
+ * @throws {Error} If client is not connected or download fails
1350
+ */
1351
+ async downloadMedia(message) {
1352
+ if (!this.isConnected) {
1353
+ throw new Error('Client is not connected');
1354
+ }
1355
+
1356
+ // Check if message has media
1357
+ const hasMedia = Boolean(
1358
+ message.raw?.message?.imageMessage ||
1359
+ message.raw?.message?.videoMessage ||
1360
+ message.raw?.message?.audioMessage ||
1361
+ message.raw?.message?.documentMessage
1362
+ );
1363
+
1364
+ if (!hasMedia) {
1365
+ return null;
1366
+ }
1367
+
1368
+ try {
1369
+ // Use Baileys' downloadMediaMessage function with the raw message
1370
+ const buffer = await downloadMediaMessage(message.raw, 'buffer', {});
1371
+
1372
+ if (buffer) {
1373
+ // Get the message content to determine file type
1374
+ const messageContent = message.raw.message;
1375
+ let mimetype = 'application/octet-stream';
1376
+
1377
+ // Determine mimetype from the message type
1378
+ if (messageContent.imageMessage) {
1379
+ mimetype = messageContent.imageMessage.mimetype || 'image/jpeg';
1380
+ } else if (messageContent.videoMessage) {
1381
+ mimetype = messageContent.videoMessage.mimetype || 'video/mp4';
1382
+ } else if (messageContent.audioMessage) {
1383
+ mimetype = messageContent.audioMessage.mimetype || 'audio/ogg';
1384
+ } else if (messageContent.documentMessage) {
1385
+ mimetype = messageContent.documentMessage.mimetype || 'application/octet-stream';
1386
+ }
1387
+
1388
+ // Use mime.getExtension() to get the proper file extension
1389
+ const extension = mime.getExtension(mimetype) || 'bin';
1390
+
1391
+ return {
1392
+ buffer,
1393
+ mimetype,
1394
+ extension,
1395
+ size: buffer.length
1396
+ };
1397
+ }
1398
+
1399
+ return null;
1400
+ } catch (error) {
1401
+ console.error('Error downloading media:', error);
1402
+ throw error;
1403
+ }
1404
+ }
1405
+
1406
+ /**
1407
+ * Parse JID information
1408
+ * @param {string} jid - The JID to parse
1409
+ * @returns {object} JID info (isLid, user, etc.)
1410
+ */
1411
+ parseJid(jid) {
1412
+ return parseJid(jid);
1413
+ }
1414
+
1415
+ /**
1416
+ * Normalize phone number to WhatsApp JID
1417
+ * @param {string} phone - Phone number
1418
+ * @returns {string} Normalized JID
1419
+ */
1420
+ normalizePhoneToJid(phone) {
1421
+ return normalizePhoneToJid(phone);
1422
+ }
1423
+
1424
+ /**
1425
+ * Plot JID (Convert between PN and LID if mapping is available)
1426
+ * @param {string} jid - JID to plot
1427
+ * @returns {string|undefined} Plotted JID
1428
+ */
1429
+ plotJid(jid) {
1430
+ return plotJid(jid);
1431
+ }
1432
+
1433
+ /**
1434
+ * Send a sticker with metadata
1435
+ * @param {string} chatId - Target chat JID
1436
+ * @param {Buffer} webpBuffer - WebP sticker buffer
1437
+ * @param {object} metadata - Sticker metadata (packName, author, etc.)
1438
+ * @returns {Promise<object>} Sent message info
1439
+ */
1440
+ async sendSticker(chatId, buffer, metadata = {}) {
1441
+ if (!this.isConnected) throw new Error('Client is not connected');
1442
+
1443
+ try {
1444
+ const sticker = new Sticker(buffer, {
1445
+ pack: metadata.packName || 'Innovators',
1446
+ author: metadata.author || 'Innovators',
1447
+ type: metadata.type || StickerTypes.FULL,
1448
+ categories: metadata.categories || [],
1449
+ id: metadata.id || 'innovators-bot',
1450
+ quality: metadata.quality || 50
1451
+ });
1452
+
1453
+ const stickerBuffer = await sticker.toBuffer();
1454
+
1455
+ return await this.sock.sendMessage(chatId, {
1456
+ sticker: stickerBuffer
1457
+ }, { ai: true });
1458
+ } catch (error) {
1459
+ console.error('Error generating sticker:', error);
1460
+ throw error;
1461
+ }
1462
+ }
1463
+
1464
+ /**
1465
+ * Add EXIF metadata to an existing WebP buffer
1466
+ * @param {Buffer} buffer - WebP buffer
1467
+ * @param {object} metadata - Metadata (packName, author)
1468
+ * @returns {Buffer} Buffer with EXIF
1469
+ */
1470
+ async addExifToSticker(buffer, metadata = {}) {
1471
+ try {
1472
+ const sticker = new Sticker(buffer, {
1473
+ pack: metadata.packName || 'Innovators',
1474
+ author: metadata.author || 'Innovators'
1475
+ });
1476
+ return await sticker.toBuffer();
1477
+ } catch (error) {
1478
+ console.error('Error adding EXIF to sticker:', error);
1479
+ throw error;
1480
+ }
1481
+ }
1482
+ }
1483
+
1484
+ function formatCode(code) {
1485
+ if (typeof code === 'string' && code.length === 8) {
1486
+ return code.slice(0, 4) + ' - ' + code.slice(4);
1487
+ }
1488
+ return code;
1489
+ }
1490
+
1491
+ module.exports = {
1492
+ WhatsAppClient: WhatsAppClient,
1493
+ Group: Group,
1494
+ }