UNPKG

innovators-bot2

Version:

A powerful WhatsApp client library that provides seamless integration between Baileys and WhatsApp-web.js style APIs. This library makes it easy to create WhatsApp bots and automation tools with a familiar interface.

886 lines (796 loc) • 35.2 kB
const { makeWASocket, Browsers, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } = require('@itsukichan/baileys'); 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 readline = require('readline'); let rl; const question = (text) => { if (rl && !rl.closed) { return new Promise((resolve) => rl.question(text, resolve)); } // Recreate readline interface if it's closed or doesn't exist rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => rl.question(text, resolve)); }; process.title = 'INNOVATORS Soft WhatsApp Server 92 322 4559543' console.log(figlet.textSync('WELCOME To')) console.log(figlet.textSync('INNOVATORS')) console.log(figlet.textSync('SOFT')) console.log(figlet.textSync('PRO')) console.log(figlet.textSync('PAKISTANI')) class Group { constructor(client, groupData) { this.client = client this.id = groupData.id 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' // Track connection state this.authmethod = config.authmethod || 'qr'; // 'qr' or 'pairing' this._reconnectDelay = 5000; // 5 seconds } async connect() { try { // Force emit 'connecting' at beginning of connection attempt if (this._connectionState !== 'connecting') { this._connectionState = 'connecting'; this.emit('connecting', 'Connecting to WhatsApp...'); } const { version, isLatest } = await fetchLatestBaileysVersion(); console.log(`Using WA version v${version.join('.')}, is latest: ${isLatest}`); const { state, saveCreds } = await useMultiFileAuthState(this.sessionName) const logger = P({ level: 'silent' }) this.sock = makeWASocket({ auth: state, logger, markOnlineOnConnect: true, syncFullHistory: true, generateHighQualityLinkPreview: true, linkPreviewImageThumbnailWidth: 192, emitOwnEvents: true }); this.sock.ev.on('connection.update', async (update) => { const { connection, lastDisconnect, qr, isNewLogin, isOnline } = update; if (qr && this.authmethod === 'qr') { this.emit('qr', qr); } if (connection === 'open') { if (this._connectionState !== 'connected') { if (this.sock?.user) { const user = this.sock.user; const userInfo = { id: user.id, name: user.name || user.notify || 'Unknown', phone: user.id.split(':')[0], platform: user.platform || 'Unknown', isOnline: true }; this.isConnected = true; this._connectionState = 'connected'; this.emit('connected', userInfo); if (rl && !rl.closed) rl.close(); } } } 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'; this.emit('disconnected', lastDisconnect?.error); } if (shouldReconnect) { this.connect(); } else if (lastDisconnect?.error?.output?.statusCode === DisconnectReason.loggedOut) { await this.reinitialize(); } } if (this.authmethod === 'pairing' && !state.creds?.registered) { const phoneNumber = await question("šŸ“± Enter your WhatsApp number (with country code): "); const customeCode = "INOVATOR" const code = await this.sock.requestPairingCode(phoneNumber.trim(), customeCode); if (code) { console.log("\nšŸ”— Pair this device using this code in WhatsApp:\n"); console.log(' ' + formatCode(code) + '\n'); console.log("Go to WhatsApp → Linked Devices → Link with code."); } else { console.log("āŒ Pairing code not found."); } } }) /** * 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) => { try { // Skip if no messages or not a notification if (!update.messages || update.messages.length === 0 || update.type !== 'notify') { return; } const [message] = update.messages; // Skip messages sent by the bot itself if (message.key.fromMe) { return; } /** * Extract message text from different possible locations in the message object * @type {string} */ let messageText = ''; let isButtonResponse = false; // Check for list response first if (message.message?.listResponseMessage) { messageText = message.message.listResponseMessage.title || message.message.listResponseMessage.description || ''; isButtonResponse = true; } // Check for button response else if (message.message?.templateButtonReplyMessage) { messageText = message.message.templateButtonReplyMessage.selectedDisplayText || message.message.templateButtonReplyMessage.selectedId || ''; isButtonResponse = true; } // Check for classic button response else if (message.message?.buttonsResponseMessage) { messageText = message.message.buttonsResponseMessage.selectedDisplayText || message.message.buttonsResponseMessage.selectedButtonId || ''; isButtonResponse = true; } // Regular message else { messageText = message.message?.conversation || message.message?.extendedTextMessage?.text || message.message?.imageMessage?.caption || message.message?.videoMessage?.caption || ''; } /** @type {string|null} */ let buttonType = null; /** @type {string} */ let buttonText = ''; // Determine message type and extract relevant data const { message: msg } = message; // Check for different types of button messages if (msg?.buttonsMessage) { buttonType = 'classic_buttons'; } // Check for hydrated template messages else if (msg?.templateMessage?.hydratedTemplate?.hydratedButtons) { buttonType = 'classic_buttons_hydrated'; } // Check for different types of hydrated four row templates else if (msg?.templateMessage?.hydratedFourRowTemplate) { const template = msg.templateMessage.hydratedFourRowTemplate; if (template.hydratedButtons) { buttonType = 'template_buttons_hydrated'; } else if (template.title) { buttonType = 'template_text_hydrated'; } else if (template.imageMessage) { buttonType = 'template_image_hydrated'; } else if (template.videoMessage) { buttonType = 'template_video_hydrated'; } else if (template.documentMessage) { buttonType = 'template_document_hydrated'; } } // Handle interactive response messages if (msg?.interactiveResponseMessage) { const interactive = msg.interactiveResponseMessage; if (interactive.nativeFlowResponse?.response?.reply) { buttonType = 'interactive_buttons_modern_native_flow'; buttonText = interactive.nativeFlowResponse.response.reply; } else if (interactive.reply) { buttonType = 'interactive_buttons_modern_reply'; buttonText = interactive.reply; } else if (interactive.buttonReplyMessage?.displayText) { buttonType = 'interactive_buttons_modern_button_reply'; buttonText = interactive.buttonReplyMessage.displayText; } } // Handle button and list responses if (msg?.listResponseMessage) { // Handle list response messages buttonType = 'list_response'; buttonText = msg.listResponseMessage.title || ''; if (msg.listResponseMessage.description) { buttonText = buttonText ? `${buttonText} - ${msg.listResponseMessage.description}` : msg.listResponse.description; } } else if (msg?.buttonsResponseMessage?.selectedButtonId) { // Handle classic button responses buttonType = 'classic_buttons'; buttonText = msg.buttonsResponseMessage.selectedDisplayText || msg.buttonsResponseMessage.selectedButtonId || ''; } else if (msg?.templateButtonReplyMessage?.selectedId) { // Handle template button replies buttonType = 'template_button_reply'; buttonText = msg.templateButtonReplyMessage.selectedDisplayText || msg.templateButtonReplyMessage.selectedId || ''; } else if (msg?.interactiveResponseMessage) { // Handle interactive responses (including list selections) const interactive = msg.interactiveResponseMessage; if (interactive.listResponse) { buttonType = 'list_selection'; buttonText = interactive.listResponse.title || interactive.listResponse.description || ''; } else if (interactive.nativeFlowResponse?.response?.reply) { buttonType = 'interactive_buttons_modern_native_flow'; buttonText = interactive.nativeFlowResponse.response.reply; } } // Prepare the message body - prioritize button/list responses over regular text const messageBody = buttonText || messageText || (msg?.listResponseMessage?.singleSelectReply?.selectedRowId) || (msg?.listResponseMessage?.title) || (msg?.listResponseMessage?.description) || ''; // Emit the message event with all extracted data this.emit('message', { from: message.key.remoteJid, body: messageText, hasMedia: Boolean(msg?.imageMessage || msg?.videoMessage || msg?.audioMessage || msg?.documentMessage), isGroup: message.key.remoteJid.endsWith('@g.us'), timestamp: message.messageTimestamp ? new Date(message.messageTimestamp * 1000) : new Date(), isButtonResponse: isButtonResponse, buttonId: message.message?.listResponseMessage?.singleSelectReply?.selectedRowId || message.message?.templateButtonReplyMessage?.selectedId || message.message?.buttonsResponseMessage?.selectedButtonId || null, buttonText: messageText, raw: message, reply: async (replyText) => { try { return await this.sock.sendMessage( message.key.remoteJid, { text: replyText }, { quoted: message } ); } catch (error) { console.error('Error sending reply:', error); throw error; } }, }); } catch (error) { console.error('Error processing message:', error); } }); // Handle incoming calls this.sock.ev.on('call', async (call) => { try { await this.emit('call', call); } catch (error) { console.error('Error in call handler:', error); this.emit('error', error); } }); // Handle credential updates this.sock.ev.on('creds.update', saveCreds); } 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 = {}) { if (!this.isConnected) { throw new Error('Client is not connected'); } let messageContent = {}; // Handle different message types if (typeof message === 'string') { messageContent = { text: message }; } else if (message && typeof message === 'object') { // Handle different message types switch (message.type) { case 'text': messageContent = { text: message.text }; if (message.mentions) { messageContent.mentions = message.mentions; } 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 } }; break; default: throw new Error('Invalid message type'); } } else { throw new Error('Invalid message content'); } try { return await this.sock.sendMessage(chatId, messageContent, 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 = {}) { if (!this.isConnected) { throw new Error('Client is not connected'); } try { // Check if file exists if (!fs.existsSync(filePath)) { throw new Error('File not found: ' + filePath); } const fileExtension = path.extname(filePath).toLowerCase(); const caption = options.caption || ''; let mediaMessage = {}; // Handle different media types switch (fileExtension) { case '.gif': case '.mp4': mediaMessage = { video: fs.readFileSync(filePath), caption: caption, gifPlayback: options.asGif || fileExtension === '.gif', } break; // Handle audio files case '.mp3': case '.ogg': case '.wav': mediaMessage = { audio: { url: filePath }, mimetype: 'audio/mp4', }; break; // Handle image files case '.jpg': case '.jpeg': case '.png': mediaMessage = { image: fs.readFileSync(filePath), caption: caption, }; break; default: throw new Error('Unsupported file type: ' + fileExtension); } return await this.sock.sendMessage(chatId, mediaMessage); } 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 = '') { if (!this.isConnected) { throw new Error('Client is not connected'); } try { // Check if file exists if (!fs.existsSync(filePath)) { throw new Error('File not found: ' + filePath); } const fileBuffer = fs.readFileSync(filePath); const fileName = path.basename(filePath); const mimeType = mime.getType(filePath); return await this.sock.sendMessage(chatId, { document: fileBuffer, caption: caption, mimetype: mimeType, fileName: fileName, }); } 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 = {}) { if (!this.isConnected) { throw new Error('Client is not connected'); } const { text, imagePath, caption, title, footer, interactiveButtons = [], hasMediaAttachment = false, } = options; let messageContent = {}; try { if (imagePath) { // Handle message with image const imageBuffer = fs.readFileSync(imagePath); messageContent = { image: imageBuffer, caption: caption, title: title, footer: footer, interactiveButtons: interactiveButtons, hasMediaAttachment: hasMediaAttachment, }; } else { // Handle text-only message messageContent = { text: text, title: title, footer: footer, interactiveButtons: interactiveButtons, }; } // Send the message with buttons return await this.sock.sendMessage(chatId, messageContent, extraOptions); } catch (error) { console.error('Error sending buttons:', error); throw error; } } /** * Send an interactive list message * @param {string} chatId - The ID of the chat to send the list to * @param {object} listOptions - Options for the list message * @param {string} listOptions.text - The text content of the message * @param {string} listOptions.title - Title of the list * @param {string} [listOptions.footer=''] - Optional footer text * @param {string} [listOptions.buttonText='Tap here'] - Text for the button * @param {Array<object>} listOptions.sections - Array of section objects * @returns {Promise<object>} The sent message info * @throws {Error} If client is not connected or message sending fails */ async SendList(chatId, listOptions) { if (!this.isConnected) { throw new Error('Client is not connected'); } try { const listMessage = { text: listOptions.text, title: listOptions.title, footer: listOptions.footer || '', buttonText: listOptions.buttonText || 'Tap here', sections: listOptions.sections.map((section) => ({ title: section.title, rows: section.rows.map((row) => ({ title: row.title, rowId: row.id, description: row.description, })), })), }; return await this.sock.sendMessage(chatId, listMessage); } catch (error) { console.error('Error sending list message:', error); throw error; } } /** * Send an external ad reply with a local image * @param {string} number - The phone number to send the ad to * @param {string} localImagePath - Path to the local image file * @param {string} title - Title of the ad * @param {string} body - Body of the ad * @returns {Promise<void>} * @throws {Error} If client is not connected or message sending fails */ async sendAdReply(number,msg, imgpath, title, body, sourceurl) { if (!this.isConnected) { throw new Error('Client is not connected'); } try { // Read the local image as Buffer const bufferLocalFile = fs.readFileSync(imgpath); // Send message with external ad reply using local image await this.sock.sendMessage(number, { text: msg, contextInfo: { externalAdReply: { title: title || 'Ad Title', body: body || 'Ad Description', mediaType: 1, // Image previewType: 0, showAdAttribution: true, renderLargerThumbnail: true, thumbnail: bufferLocalFile, sourceUrl: sourceurl || 'https://m.facebook.com/innovatorssoft', mediaUrl: sourceurl || 'https://m.facebook.com/innovatorssoft' } } }); } catch (err) { console.error('Failed to send externalAdReply:', err); } } /** * Get all groups the bot is a member of * @returns {Promise<Array<Group>>} Array of Group instances * @throws {Error} If client is not connected or an error occurs */ async getAllGroups() { if (!this.isConnected) { throw new Error('Client is not connected'); } try { const groupsData = await this.sock.groupFetchAllParticipating(); return Object.values(groupsData).map( (groupData) => new Group(this, groupData) ); } catch (error) { console.error('Error fetching groups:', error); throw error; } } /** * Add or remove participants from a group * @param {string} groupId - The ID of the group * @param {Array<string>} participantIds - Array of participant IDs to modify * @param {string} action - Action to perform ('add', 'remove', 'promote', 'demote') * @returns {Promise<Array>} Array of results for each participant update * @throws {Error} If client is not connected or an error occurs */ async changeGroupParticipants(groupId, participantIds, action) { if (!this.isConnected) { throw new Error('Client is not connected'); } const results = []; try { for (const participantId of participantIds) { const result = await this.sock.groupParticipantsUpdate( groupId, [participantId], action ); results.push(result); } return results; } catch (error) { console.error('Error updating group participants:', error); throw error; } } /** * Mark a message as read * @param {string} messageId - The ID of the message to mark as read * @returns {Promise<void>} * @throws {Error} If client is not connected or an error occurs */ async readMessage(messageId) { if (!this.isConnected) { throw new Error('Client is not connected'); } try { await this.sock.readMessages([messageId]); } catch (error) { console.error('Error marking message as read:', error); throw error; } } /** * Check if a phone number is registered on WhatsApp * @param {string} phoneNumber - The phone number to check (with country code, without '+') * @returns {Promise<boolean>} True if the number is on WhatsApp, false otherwise * @throws {Error} If an error occurs during the check */ async isNumberOnWhatsApp(phoneNumber) { if (!this.isConnected) { throw new Error('Client is not connected'); } try { const result = await this.sock.onWhatsApp(phoneNumber); return result.length > 0 && result[0].exists === true; } catch (error) { console.error('Error checking if number is on WhatsApp:', error); throw error; } } /** * Get the profile picture URL for a contact or group * @param {string} id - The contact or group JID * @returns {Promise<string|undefined>} The profile picture URL, or undefined if not available */ async getProfilePicture(id) { if (!this.isConnected) { throw new Error('Client is not connected'); } try { return await this.sock.profilePictureUrl(id); } catch (error) { // If the error is because the user has no profile picture, return undefined if (error.message && error.message.includes('404')) { return undefined; } console.error('Error getting profile picture:', error); throw error; } } /** * Reject an incoming call * @param {string} callId - The ID of the call to reject * @param {object} callInfo - Additional call information * @returns {Promise<void>} */ async rejectCall(callId, callInfo) { if (!this.isConnected) { throw new Error('Client is not connected'); } try { await this.sock.rejectCall(callId, callInfo); } catch (error) { console.error('Error rejecting call:', error); throw error; } } /** * Get the underlying socket instance * @returns {object} The socket instance */ getSock() { return this.sock; } /** * Reinitialize the WhatsApp client by clearing the session and reconnecting * @returns {Promise<void>} */ async reinitialize() { try { //console.log('Starting session reinitialization...'); // Reset connection state this.isConnected = false; // Clear the session data if it exists if (fs.existsSync(this.sessionName)) { await fs.promises.rm(this.sessionName, { recursive: true, force: true, }); //console.log('Cleared existing session data'); } // Clear any existing socket if (this.sock) { try { this.sock.ev.removeAllListeners(); await this.sock.end(undefined); } catch (e) { console.error('Error while cleaning up socket:', e); } this.sock = null; } // Add a small delay before reconnecting await new Promise(resolve => setTimeout(resolve, 1000)); // Reconnect with a fresh session //console.log('Establishing new connection...'); await this.connect(1); // Start with attempt 1 this.emit('reinitialized'); } catch (error) { console.error('Error during reinitialization:', error); this.emit('error', error); throw error; } } /** * Log out from WhatsApp * @returns {Promise<boolean>} True if logout was successful * @throws {Error} If logout fails */ async logout() { if (!this.sock) { this.emit('logout', 'Already logged out'); return true; } try { // Properly close the socket connection await this.sock.logout(); await this.sock.end(); this.sock = null; // Remove session data if it exists if (fs.existsSync(this.sessionName)) { fs.rmSync(this.sessionName, { recursive: true, force: true, }); } // Update connection state and emit event this.isConnected = false; this.emit('logout', 'Logged out successfully'); return true; } catch (error) { console.error('Logout error:', error); this.emit('error', new Error(`Failed to logout: ${error.message}`)); throw error; } } } function formatCode(code) { if (typeof code === 'string' && code.length === 8) { return code.slice(0, 4) + ' - ' + code.slice(4); } return code; } module.exports = { WhatsAppClient: WhatsAppClient, Group: Group, }