import ky from "ky"; import logger from "../utils/logger.js"; import FormData from "form-data"; import fs from "fs"; import path from "path"; // Function to escape special characters for MarkdownV2 const escapeMarkdown = (text) => { return text.replace(/([_*\[\]()~`>#+=|{}.!\\-])/g, '\\$1'); }; // Function to preserve markdown formatting while escaping other special characters const preserveMarkdown = (text) => { // First, temporarily replace valid markdown text = text .replace(/\*\*(.*?)\*\*/g, '%%%BOLD%%%$1%%%BOLD%%%') .replace(/__(.*?)__/g, '%%%ITALIC%%%$1%%%ITALIC%%%') .replace(/`(.*?)`/g, '%%%CODE%%%$1%%%CODE%%%') .replace(/\[(.*?)\]\((.*?)\)/g, '%%%LINK_TEXT%%%$1%%%LINK_URL%%%$2%%%LINK%%%'); // Escape all special characters text = escapeMarkdown(text); // Restore markdown formatting return text .replace(/%%%BOLD%%%(.*?)%%%BOLD%%%/g, '*$1*') .replace(/%%%ITALIC%%%(.*?)%%%ITALIC%%%/g, '_$1_') .replace(/%%%CODE%%%(.*?)%%%CODE%%%/g, '`$1`') .replace(/%%%LINK_TEXT%%%(.*?)%%%LINK_URL%%%(.*?)%%%LINK%%%/g, '[$1]($2)'); }; /** * Sends a message via a Telegram bot. * @param {string} telegramToken - The bot's API token. * @param {number} chatId - The recipient's Telegram chat ID. * @param {string} message - The message text. * @param {string} [photoPath] - Optional path to photo file. * @returns {Promise} - Returns `true` if the message is sent successfully, otherwise `false`. */ export const sendTelegramMessage = async (telegramToken, chatId, message, photoPath = null) => { try { // Process message text if it exists const processedMessage = message?.trim() ? preserveMarkdown(message) : ''; if (photoPath) { const formData = new FormData(); formData.append('chat_id', chatId); // Debug information before reading file logger.info('Attempting to read file:', { photoPath, exists: fs.existsSync(photoPath), stats: fs.existsSync(photoPath) ? fs.statSync(photoPath) : null, dirname: path.dirname(photoPath), basename: path.basename(photoPath) }); try { // Ensure the file exists before reading if (!fs.existsSync(photoPath)) { throw new Error(`File does not exist at path: ${photoPath}`); } // Create read stream and append to FormData const photoStream = fs.createReadStream(photoPath); formData.append('photo', photoStream); if (processedMessage) { formData.append('caption', processedMessage); formData.append('parse_mode', 'MarkdownV2'); } // Debug the FormData contents logger.info('FormData created:', { boundaryUsed: formData.getBoundary?.() || 'No boundary found', headers: formData.getHeaders?.() || 'No headers available', messageLength: processedMessage.length }); // Wait for the entire request to complete await new Promise((resolve, reject) => { formData.submit({ host: 'api.telegram.org', path: `/bot${telegramToken}/sendPhoto`, protocol: 'https:', }, (err, res) => { if (err) { reject(err); return; } let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { try { const response = JSON.parse(data); if (!response.ok) { reject(new Error(response.description || 'Failed to send photo')); } else { resolve(response); } } catch (e) { reject(new Error('Failed to parse Telegram response')); } }); }); }); } catch (error) { // Log detailed error information logger.error(`❌ Telegram API Error for chat ${chatId}:`, { error: error.message, response: error.response ? await error.response.json().catch(() => ({})) : undefined, photoPath, messageLength: processedMessage.length, fileExists: fs.existsSync(photoPath), fileStats: fs.existsSync(photoPath) ? fs.statSync(photoPath) : null }); throw error; } } else { try { await ky.post(`https://api.telegram.org/bot${telegramToken}/sendMessage`, { json: { chat_id: chatId, text: processedMessage, parse_mode: "MarkdownV2", }, timeout: 5000, }); } catch (error) { // Log detailed error information logger.error(`❌ Telegram API Error for chat ${chatId}:`, { error: error.message, response: await error.response?.json().catch(() => ({})), messageLength: processedMessage.length }); throw error; } } logger.info(`✅ Telegram message sent to ${chatId}`); return true; } catch (error) { logger.error(`❌ Failed to send Telegram message to ${chatId}: ${error.message}`); return false; } }; /** * Sends messages in bulk while respecting Telegram's rate limits. * @param {string} telegramToken - The bot's API token. * @param {Array} chatIds - An array of chat IDs to send to. * @param {string} message - The message text. * @param {string} [photoPath] - Optional path to photo file. * @returns {Promise} */ export const sendBulkTelegramMessages = async (telegramToken, chatIds, message, photoPath = null) => { for (let i = 0; i < chatIds.length; i++) { const chatId = chatIds[i]; await sendTelegramMessage(telegramToken, chatId, message, photoPath); if ((i + 1) % 30 === 0) { logger.info(`⏳ Rate limit pause (sent ${i + 1} messages)...`); await new Promise((resolve) => setTimeout(resolve, 1000)); } } };