171 lines
5.8 KiB
JavaScript
171 lines
5.8 KiB
JavaScript
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<boolean>} - 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<number>} 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<void>}
|
|
*/
|
|
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));
|
|
}
|
|
}
|
|
}; |