Files
ember-market-frontend/backend/utils/telegramUtils.js
2025-03-10 17:39:37 +00:00

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));
}
}
};