UNPKG

innovators-bot2

Version:
1,114 lines (988 loc) 121 kB
const { makeWASocket, Browsers, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, downloadMediaMessage, getCurrentSenderInfo, // JID Utilities parseJid, plotJid, normalizePhoneToJid, // Anti-Delete MessageStore, createMessageStoreHandler, createAntiDeleteHandler, createTypingIndicator, generateInteractiveButtonMessage, generateInteractiveListMessage, generateCombinedButtons, generateCopyCodeButton, generateUrlButtonMessage, generateQuickReplyButtons, StatusHelper, STATUS_BACKGROUNDS, STATUS_FONTS, renderLatexToPng, uploadUnencryptedToWA, RichSubMessageType, getAggregateVotesInPollMessage } = require('@innovatorssoft/baileys'); const { Sticker, StickerTypes } = require('wa-sticker-formatter'); const { Boom } = require('@hapi/boom'); const { EventEmitter } = require('events'); const P = require('pino'); const fs = require('fs'); const path = require('path'); const mime = require('mime'); const figlet = require('figlet'); const NodeCache = require('node-cache'); process.title = 'INNOVATORS Soft WhatsApp Server +447498792682' console.log(figlet.textSync('WELCOME To')) console.log(figlet.textSync('INNOVATORS')) console.log(figlet.textSync('SOFT')) class Group { constructor(client, groupData) { this.client = client this.id = groupData.id this.notify = groupData.notify this.subject = groupData.subject this.creation = groupData.creation this.owner = groupData.owner this.desc = groupData.desc this.participants = groupData.participants } } class WhatsAppClient extends EventEmitter { constructor(config = {}) { super() this.sock = null this.isConnected = false this.sessionName = config.sessionName || 'auth_info_baileys' this._connectionState = 'disconnected' this.authmethod = config.authmethod || 'qr'; this._reconnectDelay = 5000; this.pairingPhoneNumber = config.pairingPhoneNumber || null; this.groupMetadataCache = new NodeCache({ stdTTL: 600, checkperiod: 120 }); this.messageStore = new MessageStore({ maxMessagesPerChat: config.maxMessagesPerChat || 1000, ttl: config.messageTTL || 24 * 60 * 60 * 1000 }); // Initialize contacts cache this.contactsCache = new NodeCache({ stdTTL: 0, checkperiod: 20 }); // No TTL, manual cleanup on logout // Message store persistence configuration this.messageStoreFilePath = config.messageStoreFilePath || path.join(this.sessionName, 'message-store.json'); this.autoSaveInterval = config.autoSaveInterval || 5 * 60 * 1000; // Default: 5 minutes this._autoSaveTimer = null; this._storeChangeCount = 0; this._pairingCodeTimer = null; this._lastStoreSave = null; this.ai = config.ai === undefined ? true : config.ai; } /** * Helper method to resolve LID to PN (Phone Number) if available and normalize JID * @param {string} jid - The JID to resolve (could be LID or PN) * @returns {Promise<string>} The resolved and normalized PN if LID mapping exists, otherwise the normalized original JID * @private */ async _resolveLidToPn(jid) { if (!jid) return jid; // If it's a LID, try to resolve it to PN if (jid.endsWith('@lid')) { try { const phoneNumber = await this.getPNForLID(jid); if (phoneNumber) { // Normalize the resolved PN by removing device ID return this._normalizeJid(phoneNumber); } return jid; // Return original LID if no PN found } catch (error) { console.error('Error resolving LID to PN:', error); return jid; // Return original JID on error } } // If it's already a PN or other format, normalize and return return this._normalizeJid(jid); } /** * Normalize JID by removing device ID suffix (e.g., :0) * Converts 923014434335:0@s.whatsapp.net to 923014434335@s.whatsapp.net * @param {string} jid - The JID to normalize * @returns {string} Normalized JID * @private */ _normalizeJid(jid) { if (!jid) return jid; // Remove device ID (e.g., :0, :1, etc.) from the JID // Pattern: number:deviceId@server becomes number@server return jid.replace(/:\d+@/, '@'); } /** * Internal helper to handle mentions and the "mention all" flag * @param {string[]} mentions - Array of JIDs or keywords like 'all'/'@all' * @param {boolean} mentionAll - Explicit mentionAll flag * @returns {object} Object containing processed mentions and mentionAll flag * @private */ _handleMentions(mentions, mentionAll) { let processedMentions = mentions; let finalMentionAll = mentionAll; if (mentions && Array.isArray(mentions)) { processedMentions = mentions .filter(jid => jid !== 'all' && jid !== '@all') .map(jid => this._normalizeJid(jid)); if (mentions.includes('all') || mentions.includes('@all')) { finalMentionAll = true; } } return { mentions: processedMentions, mentionAll: finalMentionAll }; } async connect() { try { if (this._connectionState === 'connecting' && this.sock) { return; // Prevent concurrent connection attempts } if (this._connectionState !== 'connecting') { this._connectionState = 'connecting'; this.emit('connecting', 'Connecting to WhatsApp...'); } const { version: baileysVersion, isLatest: baileysIsLatest } = await fetchLatestBaileysVersion(); console.log('Using Baileys Version:', baileysVersion, baileysIsLatest ? ' isLatest true' : ' isLatest false'); const { state, saveCreds } = await useMultiFileAuthState(this.sessionName) const logger = P({ level: 'silent' }) this.sock = makeWASocket({ auth: state, logger, markOnlineOnConnect: false, syncFullHistory: false, getMessage: async (key) => { const msg = this.messageStore.getOriginalMessage(key); if (!msg) { console.log(`Message not found for key: ${JSON.stringify(key)}`); return undefined; } return msg.message; }, generateHighQualityLinkPreview: true, linkPreviewImageThumbnailWidth: 192, emitOwnEvents: true, browser: Browsers.android('Innovators Soft'), version: baileysVersion, cachedGroupMetadata: async (jid) => { const cached = this.groupMetadataCache.get(jid); if (cached) { return cached; } try { const metadata = await this.sock.groupMetadata(jid); this.groupMetadataCache.set(jid, metadata); return metadata; } catch (error) { console.error(`Error fetching metadata for group ${jid}:`, error); return null; } }, }); this.store = this.sock.signalRepository.lidMapping; this.sock.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => { if (connection === 'close' && this._pairingCodeTimer) { clearTimeout(this._pairingCodeTimer); this._pairingCodeTimer = null; } if (qr && this.authmethod === 'qr') { this.emit('qr', qr); } if (connection === 'open') { if (this._connectionState !== 'connected') { const user = getCurrentSenderInfo(this.sock.authState) if (user) { this.isConnected = true; const userInfo = { name: user.pushName || 'Unknown', phone: user.phoneNumber, platform: user.platform || 'Unknown', isOnline: true, }; this._connectionState = 'connected'; this.emit('connected', userInfo); // Load message store from file await this.loadMessageStore(); // Start auto-save this._startAutoSave(); } } } else if (connection === 'close') { const shouldReconnect = (lastDisconnect?.error instanceof Boom) ? lastDisconnect.error?.output?.statusCode !== DisconnectReason.loggedOut : true; if (this._connectionState !== 'disconnected') { this.isConnected = false; this._connectionState = 'disconnected'; // Save message store before disconnecting await this.saveMessageStore(); // Stop auto-save this._stopAutoSave(); this.emit('disconnected', lastDisconnect?.error); } if (shouldReconnect) { this.connect(); } else if (lastDisconnect?.error?.output?.statusCode === DisconnectReason.loggedOut) { await this.reinitialize(); } } // Handle pairing code request after connection is established but before login if (this.authmethod === 'pairing' && connection === 'connecting' && !state.creds?.registered) { const phoneNumber = this.pairingPhoneNumber; if (phoneNumber) { try { // Wait a bit for the connection to initialize properly if (this._pairingCodeTimer) { clearTimeout(this._pairingCodeTimer); } this._pairingCodeTimer = setTimeout(async () => { const customeCode = "INOVATOR"; try { if (!this.sock || this._connectionState === 'disconnected') { const err = new Error('Socket is not available to request pairing code'); console.error('Error requesting pairing code:', err); this.emit('error', err); return; } const code = await this.sock.requestPairingCode(phoneNumber, customeCode); if (code) { // Emit pairing code event so clients can handle it this.emit('pairing-code', formatCode(code)); } else { console.log("❌ Pairing code not found."); } } catch (error) { console.error('Error requesting pairing code:', error); this.emit('error', error); } finally { this._pairingCodeTimer = null; } }, 2000); // Wait 2 seconds before requesting pairing code } catch (error) { console.error('Error setting timeout for pairing code:', error); this.emit('error', error); } } } }) /** * Handle incoming messages * @emits WhatsAppClient#message - When a new message is received * @param {object} update - The message update object */ this.sock.ev.on('messages.upsert', async (update) => { // 📝 Store message for the Anti-Delete system const storeHandler = createMessageStoreHandler(this.messageStore); storeHandler(update); // 📢 Emit event that messages were stored this.emit('message-stored', update.messages); // Increment change counter for auto-save this._incrementStoreChangeCount(); // Save message store to file immediately await this.saveMessageStore(); /*console.log('-'.repeat(50)); console.dir(update, { depth: null }); console.log('-'.repeat(50));*/ try { if (update.type !== 'notify' || !update.messages?.length) return; const [message] = update.messages; if (!message || message.key?.fromMe) return; // 🚫 Ignore all protocol messages (history sync, security notifications, app state sync, deleted messages, etc.) if (message.message?.protocolMessage) return; const msg = message.message || {}; let jid = this._normalizeJid(message.key.remoteJid); // Keep the original technical JID as the primary identifier for Signal sessions // remoteJidAlt can be used if needed, but not to replace the primary jid for technical replies const jidAlt = this._normalizeJid(message.key.remoteJidAlt) || null; // Resolve the actual sender (preferring PN over LID) const participant = this._normalizeJid(message.key.participant || message.participant) || null; const participantAlt = this._normalizeJid(message.key.participantAlt) || null; let sender = jid; if (jid.endsWith('@g.us') || jid === 'status@broadcast') { sender = (participantAlt && participantAlt.endsWith('@s.whatsapp.net')) ? participantAlt : (participant || jid); } // Resolve LID to PN for sender if needed sender = await this._resolveLidToPn(sender); const timestamp = new Date((message.messageTimestamp || Date.now()) * 1000); const getText = () => msg.conversation || msg.extendedTextMessage?.text || msg.imageMessage?.caption || msg.videoMessage?.caption || ''; const getButtonText = () => { if (msg.listResponseMessage) return msg.listResponseMessage.title || msg.listResponseMessage.description || ''; if (msg.templateButtonReplyMessage) return msg.templateButtonReplyMessage.selectedDisplayText || msg.templateButtonReplyMessage.selectedId || ''; if (msg.buttonsResponseMessage) return msg.buttonsResponseMessage.selectedDisplayText || msg.buttonsResponseMessage.selectedButtonId || ''; if (msg.interactiveResponseMessage) { const i = msg.interactiveResponseMessage; return i.listResponse?.title || i.listResponse?.description || i.nativeFlowResponse?.response?.reply || i.reply || i.buttonReplyMessage?.displayText || ''; } return ''; }; const reply = async (text) => this.sock.sendMessage(jid, { text }, { quoted: message, ai: this.ai }); // ✅ Handle status updates (stories) if (jid === 'status@broadcast') { this.emit('status', { from: sender, sender, participant, participantAlt, body: getText(), hasMedia: Boolean(msg.imageMessage || msg.videoMessage), timestamp, key: message.key, raw: message, // Reply to status reply: async (text) => { if (!sender) throw new Error('Missing participant JID'); return this.sock.sendMessage(sender, { text }, { quoted: message, ai: this.ai }); }, // 👍 Like (react) to status like: async (emoji = '❤️') => { if (!sender) throw new Error('Missing participant JID'); // Read the message first try { await this.sock.readMessages([message.key]); } catch (readError) { console.error('Error reading status message:', readError); } // Then send the reaction return this.sock.sendMessage(sender, { react: { text: emoji, key: message.key } }, { ai: this.ai }); } }); return; } // ✅ Handle normal/chat messages const buttonText = getButtonText(); const body = buttonText || getText() || msg.listResponseMessage?.singleSelectReply?.selectedRowId || ''; this.emit('message', { from: jid, fromAlt: jidAlt, sender, participant, participantAlt, body, hasMedia: Boolean(msg.imageMessage || msg.videoMessage || msg.audioMessage || msg.documentMessage), isGroup: jid.endsWith('@g.us'), timestamp, isButtonResponse: Boolean(buttonText), buttonId: msg?.listResponseMessage?.singleSelectReply?.selectedRowId || msg?.templateButtonReplyMessage?.selectedId || msg?.buttonsResponseMessage?.selectedButtonId || null, buttonText, raw: message, reply, }); } catch (err) { console.error('Error processing message:', err); } }); // 🛡️ Anti-Delete System: Handle message revokes/deletions const antiDeleteHandler = createAntiDeleteHandler(this.messageStore); this.sock.ev.on('messages.update', async (updates) => { const deletedMessages = antiDeleteHandler(updates); for (const info of deletedMessages) { let jid = this._normalizeJid(info.key.remoteJid); // Use original remoteJid for technical identification const jidAlt = this._normalizeJid(info.key.remoteJidAlt) || null; this.emit('message-deleted', { jid: jid, jidAlt: jidAlt, originalMessage: info.originalMessage, key: info.key }); } // Handle Poll Updates for (const updateObj of updates) { const { key, update } = updateObj; if (update.pollUpdates) { try { const pollCreation = this.messageStore.getOriginalMessage(key); if (pollCreation) { // Initialize pollUpdates array on the stored message if it doesn't exist if (!pollCreation.pollUpdates) { pollCreation.pollUpdates = []; } // Merge new updates by voter JID to ensure only the latest vote per voter is stored for (const newUp of update.pollUpdates) { const voterJid = newUp.pollUpdateMessageKey?.participant || (newUp.pollUpdateMessageKey?.fromMe ? 'me' : null); if (voterJid) { const normVoterJid = voterJid === 'me' ? 'me' : this._normalizeJid(voterJid); const index = pollCreation.pollUpdates.findIndex(existing => { const existingVoter = existing.pollUpdateMessageKey?.participant || (existing.pollUpdateMessageKey?.fromMe ? 'me' : null); const normExisting = existingVoter === 'me' ? 'me' : this._normalizeJid(existingVoter); return normExisting === normVoterJid; }); if (index !== -1) { pollCreation.pollUpdates[index] = newUp; // Replace with latest vote update from this voter } else { pollCreation.pollUpdates.push(newUp); // Add new voter's update } } } const pollUpdate = getAggregateVotesInPollMessage({ message: pollCreation.message || pollCreation, pollUpdates: pollCreation.pollUpdates, }); // Resolve JID from LID to PN for voters in the poll update const resolvedPollUpdate = await Promise.all( pollUpdate.map(async (option) => { const resolvedVoters = await Promise.all( (option.voters || []).map(async (v) => { if (v === 'me') return 'me'; return await this._resolveLidToPn(v); }) ); return { ...option, voters: resolvedVoters }; }) ); let jid = this._normalizeJid(key.remoteJid); jid = await this._resolveLidToPn(jid); const jidAlt = this._normalizeJid(key.remoteJidAlt) || null; // Clone key to resolve LIDs to PNs without mutating the original reference if it's read-only const resolvedKey = { ...key }; if (key.remoteJid) { resolvedKey.remoteJid = await this._resolveLidToPn(key.remoteJid); } if (key.participant) { resolvedKey.participant = await this._resolveLidToPn(key.participant); } // Clone pollCreation to resolve LIDs to PNs without mutating the original store reference const resolvedPollCreation = { ...pollCreation }; if (pollCreation.participant) { resolvedPollCreation.participant = await this._resolveLidToPn(pollCreation.participant); } if (pollCreation.key) { resolvedPollCreation.key = { ...pollCreation.key }; if (pollCreation.key.remoteJid) { resolvedPollCreation.key.remoteJid = await this._resolveLidToPn(pollCreation.key.remoteJid); } if (pollCreation.key.participant) { resolvedPollCreation.key.participant = await this._resolveLidToPn(pollCreation.key.participant); } } // Extract and resolve voter JID(s) from the pollUpdates const voters = await Promise.all( (update.pollUpdates || []).map(async (u) => { const voterJid = u.pollUpdateMessageKey?.participant || (u.pollUpdateMessageKey?.fromMe ? 'me' : null); if (voterJid && voterJid !== 'me') { return await this._resolveLidToPn(this._normalizeJid(voterJid)); } return voterJid; }) ).then(arr => arr.filter(Boolean)); this.emit('poll-votes-update', { jid: jid, jidAlt: jidAlt, voter: voters[0] || null, voters: voters, key: resolvedKey, pollUpdate: resolvedPollUpdate, pollCreationMessage: resolvedPollCreation }); } else { console.log('[PollVotes] Could not find poll creation message in store for key:', key.id); } } catch (err) { console.error('[PollVotes ERROR] Error processing poll updates:', err); } } } }); // 👍 Handle message reactions this.sock.ev.on('messages.reaction', async (reactions) => { try { for (const reaction of reactions) { if (reaction.key?.fromMe) continue; // Get the chat JID, preferring PN over LID let jid = this._normalizeJid(reaction.key.remoteJid); // Use original remoteJid for technical identification const jidAlt = this._normalizeJid(reaction.key.remoteJidAlt) || null; // Resolve the sender (who reacted), preferring PN over LID const participant = this._normalizeJid(reaction.key.participant) || null; const participantAlt = this._normalizeJid(reaction.key.participantAlt) || null; let sender = jid; if (jid.endsWith('@g.us') || jid === 'status@broadcast') { sender = (participantAlt && participantAlt.endsWith('@s.whatsapp.net')) ? participantAlt : (participant || jid); } // Resolve LID to PN for sender if needed sender = await this._resolveLidToPn(sender); // Emit the reaction event this.emit('message-reaction', { from: jid, fromAlt: jidAlt, sender: sender, participant: participant, participantAlt: participantAlt, emoji: reaction.reaction?.text || null, isRemoved: !reaction.reaction?.text, messageKey: reaction.key, timestamp: new Date(), raw: reaction }); } } catch (error) { console.error('Error processing message reaction:', error); } }); // Handle incoming calls this.sock.ev.on('call', async (call) => { try { // Extract phone number from LID if available for (const callData of call) { if (callData.chatId || callData.from) { const jid = callData.chatId || callData.from; // Resolve LID to PN using the helper method const resolvedJid = await this._resolveLidToPn(jid); callData.phoneNumber = resolvedJid.split(':')[0].split('@')[0]; } } await this.emit('call', call); } catch (error) { console.error('Error in call handler:', error); this.emit('error', error); } }); // Handle LID/PN mapping updates this.sock.ev.on('lid-mapping.update', async (update) => { try { // Store the mapping for future use if (update && Object.keys(update).length > 0) { // The update object contains PN -> LID mappings // They are automatically stored in sock.signalRepository.lidMapping this.emit('lid-mapping-update', update); } } catch (error) { console.error('Error processing LID mapping update:', error); } }); // Handle credential updates this.sock.ev.on('creds.update', saveCreds); // Handle group events to keep the cache updated this.sock.ev.on('groups.upsert', (groups) => { for (const group of groups) { this.updateGroupMetadataCache(group.id, group); } }); this.sock.ev.on('groups.update', (groups) => { for (const group of groups) { // Get existing cache and update it with new information const cached = this.groupMetadataCache.get(group.id); if (cached) { // Merge the updated fields with existing cached data const updated = { ...cached, ...group }; this.updateGroupMetadataCache(group.id, updated); } } }); this.sock.ev.on('group-participants.update', async (update) => { // When participants change, refresh the group metadata try { const metadata = await this.sock.groupMetadata(update.id); this.updateGroupMetadataCache(update.id, metadata); } catch (error) { // Check if group no longer exists or bot was removed (404 item-not-found) const isNotFound = error.data === 404 || error.message?.includes('item-not-found') || error.output?.statusCode === 404; if (isNotFound) { // Group no longer exists or bot was removed - clean up cache this.clearGroupMetadataCache(update.id); this.emit('group-left', { id: update.id, reason: 'Group not found or bot was removed' }); } else { console.error(`Error refreshing metadata for group ${update.id}:`, error); } } }); // Handle contacts from history sync this.sock.ev.on('messaging-history.set', ({ contacts: newContacts }) => { if (newContacts && newContacts.length > 0) { for (const contact of newContacts) { this.contactsCache.set(contact.id, contact); } this.emit('contacts-received', newContacts); } }); // Handle contacts upsert (new contacts added) this.sock.ev.on('contacts.upsert', (newContacts) => { for (const contact of newContacts) { this.contactsCache.set(contact.id, contact); } this.emit('contacts-upsert', newContacts); }); // Handle contacts update (profile picture changes, etc.) this.sock.ev.on('contacts.update', (updates) => { for (const update of updates) { const existing = this.contactsCache.get(update.id) || {}; this.contactsCache.set(update.id, { ...existing, ...update }); } this.emit('contacts-update', updates); }); } catch (error) { console.error('Error in connect:', error); this.emit('error', error); throw error; } } /** * Send a message to a chat * @param {string} chatId - The ID of the chat to send the message to * @param {string|object} message - The message content (string) or message object * @param {object} options - Additional options for sending the message * @returns {Promise<object>} The sent message info * @throws {Error} If client is not connected or message sending fails */ async sendMessage(chatId, message, options = {}) { chatId = this._normalizeJid(chatId); if (!this.isConnected) { throw new Error('Client is not connected'); } let messageContent = {}; // Check if poll is provided in message or options let pollData = null; if (message && typeof message === 'object' && message.poll) { pollData = message.poll; } else if (options && options.poll) { pollData = options.poll; } if (pollData) { messageContent = { poll: { name: pollData.name, values: pollData.values || pollData.options || [], selectableCount: pollData.selectableCount !== undefined ? pollData.selectableCount : (pollData.selectableOptionsCount !== undefined ? pollData.selectableOptionsCount : 1), toAnnouncementGroup: pollData.toAnnouncementGroup || false } }; } else if (typeof message === 'string') { messageContent = { text: message }; } else if (message && typeof message === 'object') { if (message.richResponse) { if (Array.isArray(message.richResponse)) { // Route array of submessages to sendRichMessage instead return await this.sendRichMessage(chatId, message.richResponse, options.quoted || null, { ...options, useMarkdown: true }); } messageContent = { richResponse: message.richResponse }; } else { // Handle different message types switch (message.type) { case 'text': messageContent = { text: message.text }; const { mentions: textMentions, mentionAll: textMentionAll } = this._handleMentions(message.mentions, message.mentionAll); if (textMentions) messageContent.mentions = textMentions; if (textMentionAll !== undefined) messageContent.mentionAll = textMentionAll; break; case 'location': messageContent = { location: { degreesLatitude: message.latitude, degreesLongitude: message.longitude, name: message.name, address: message.address } }; break; case 'contact': messageContent = { contacts: { displayName: message.fullName, contacts: [{ displayName: message.fullName, vcard: `BEGIN:VCARD\nVERSION:3.0\n` + `FN:${message.fullName}\n` + (message.organization ? `ORG:${message.organization};\n` : '') + (message.phoneNumber ? `TEL;type=CELL;type=VOICE;waid=${message.phoneNumber}:+${message.phoneNumber}\n` : '') + 'END:VCARD' }] } }; break; case 'reaction': messageContent = { react: { text: message.emoji, key: message.messageKey || message.message?.key || message.key } }; break; default: throw new Error('Invalid message type'); } } } else { throw new Error('Invalid message content'); } try { return await this.sock.sendMessage(chatId, messageContent, { ai: this.ai, ...options }); } catch (error) { console.error('Error sending message:', error); throw error; } } /** * Send a media file to a chat * @param {string} chatId - The ID of the chat to send the media to * @param {string} filePath - Path to the media file * @param {object} options - Additional options for the media message * @returns {Promise<object>} The sent message info * @throws {Error} If client is not connected or file not found */ async sendMedia(chatId, filePath, options = {}) { chatId = this._normalizeJid(chatId); if (!this.isConnected) { throw new Error('Client is not connected'); } try { let fileBuffer; let fileExtension; let isUrl = false; try { const parsedUrl = new URL(filePath); isUrl = parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'; } catch (_) { } if (isUrl) { const response = await fetch(filePath); if (!response.ok) { throw new Error(`Failed to fetch media from URL: ${response.statusText}`); } const arrayBuffer = await response.arrayBuffer(); fileBuffer = Buffer.from(arrayBuffer); const contentType = response.headers.get('content-type'); if (contentType) { const cleanMime = contentType.split(';')[0].trim(); fileExtension = '.' + mime.getExtension(cleanMime); } else { const parsedUrl = new URL(filePath); fileExtension = path.extname(parsedUrl.pathname).toLowerCase(); } } else { // Check if file exists if (!fs.existsSync(filePath)) { throw new Error('File not found: ' + filePath); } fileBuffer = fs.readFileSync(filePath); fileExtension = path.extname(filePath).toLowerCase(); } const caption = options.caption || ''; let mediaMessage = {}; // Handle different media types switch (fileExtension) { case '.gif': case '.mp4': mediaMessage = { video: fileBuffer, caption: caption, gifPlayback: options.asGif || fileExtension === '.gif', } break; // Handle audio files case '.mp3': case '.ogg': case '.wav': mediaMessage = { audio: fileBuffer, mimetype: 'audio/mp4', }; break; // Handle image files case '.jpg': case '.jpeg': case '.png': mediaMessage = { image: fileBuffer, caption: caption, }; break; default: throw new Error('Unsupported file type: ' + fileExtension); } const { mentions: mediaMentions, mentionAll: mediaMentionAll } = this._handleMentions(options.mentions, options.mentionAll); if (mediaMentions) mediaMessage.mentions = mediaMentions; if (mediaMentionAll !== undefined) mediaMessage.mentionAll = mediaMentionAll; return await this.sock.sendMessage(chatId, mediaMessage, { ai: this.ai }); } catch (error) { console.error('Error sending media:', error); throw error; } } /** * Send a document to a chat * @param {string} chatId - The ID of the chat to send the document to * @param {string} filePath - Path to the document file * @param {string} [caption=''] - Optional caption for the document * @returns {Promise<object>} The sent message info * @throws {Error} If client is not connected or file not found */ async sendDocument(chatId, filePath, caption = '') { chatId = this._normalizeJid(chatId); if (!this.isConnected) { throw new Error('Client is not connected'); } try { let fileBuffer; let fileName; let mimeType; let isUrl = false; try { const parsedUrl = new URL(filePath); isUrl = parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'; } catch (_) { } if (isUrl) { const response = await fetch(filePath); if (!response.ok) { throw new Error(`Failed to fetch document from URL: ${response.statusText}`); } const arrayBuffer = await response.arrayBuffer(); fileBuffer = Buffer.from(arrayBuffer); const contentType = response.headers.get('content-type'); if (contentType) { mimeType = contentType.split(';')[0].trim(); } else { mimeType = mime.getType(filePath) || 'application/octet-stream'; } const contentDisposition = response.headers.get('content-disposition'); if (contentDisposition) { const match = contentDisposition.match(/filename="?([^"]+)"?/); if (match && match[1]) { fileName = match[1]; } } if (!fileName) { const parsedUrl = new URL(filePath); fileName = path.basename(parsedUrl.pathname) || 'document'; } } else { if (!fs.existsSync(filePath)) { throw new Error('File not found: ' + filePath); } fileBuffer = fs.readFileSync(filePath); fileName = path.basename(filePath); mimeType = mime.getType(filePath) || 'application/octet-stream'; } const messageContent = { document: fileBuffer, caption: caption, mimetype: mimeType, fileName: fileName, }; if (typeof caption === 'object' && caption !== null) { if (caption.caption) messageContent.caption = caption.caption; const { mentions: docMentions, mentionAll: docMentionAll } = this._handleMentions(caption.mentions, caption.mentionAll); if (docMentions) messageContent.mentions = docMentions; if (docMentionAll !== undefined) messageContent.mentionAll = docMentionAll; } return await this.sock.sendMessage(chatId, { ...messageContent, }, { ai: this.ai }); } catch (error) { console.error('Error sending document:', error); throw error; } } /** * Send a message with interactive buttons * @param {string} chatId - The ID of the chat to send the message to * @param {object} options - Options for the button message * @param {string} [options.text] - The text content of the message * @param {string} [options.imagePath] - Optional path to an image to include * @param {string} [options.caption] - Caption for the image * @param {string} [options.title] - Title for the message * @param {string} [options.footer] - Footer text for the message * @param {Array} [options.interactiveButtons=[]] - Array of button objects * @param {boolean} [options.hasMediaAttachment=false] - Whether the message has a media attachment * @param {object} [extraOptions={}] - Additional options for the message * @returns {Promise<object>} The sent message info * @throws {Error} If client is not connected or message sending fails */ async sendButtons(chatId, options = {}, extraOptions = {}) { chatId = this._normalizeJid(chatId); if (!this.isConnected) { throw new Error('Client is not connected'); } const { text, imagePath, image, video, document, location, product, mimetype, jpegThumbnail, caption, title, subtitle, footer, interactiveButtons = [], hasMediaAttachment = false, } = options; let messageContent = {}; try { const base = { title: title, subtitle: subtitle, footer: footer, interactiveButtons: interactiveButtons, hasMediaAttachment: hasMediaAttachment, }; if (imagePath) { // Handle message with local image path const imageBuffer = fs.readFileSync(imagePath); messageContent = { ...base, image: imageBuffer, caption: caption, }; } else if (image || video || document || location || product) { // Pass-through media objects (e.g. { image: { url } }) messageContent = { ...base, ...(image ? { image } : {}), ...(video ? { video } : {}), ...(document ? { document } : {}), ...(location ? { location } : {}), ...(product ? { product } : {}), ...(mimetype ? { mimetype } : {}), ...(jpegThumbnail ? { jpegThumbnail } : {}), caption: caption, }; } else { // Handle text-only message messageContent = { ...base, text: text, }; } //