other
This commit is contained in:
171
backend/utils/telegramUtils.js
Normal file
171
backend/utils/telegramUtils.js
Normal file
@@ -0,0 +1,171 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user