innovators-bot2
Version:
1,114 lines (988 loc) • 121 kB
JavaScript
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,
};
}
//