2026-05-18 07:06:21 +07:00
|
|
|
import { Telegraf } from 'telegraf';
|
2026-05-18 07:27:06 +07:00
|
|
|
import { config } from '../env';
|
2026-05-18 07:29:01 +07:00
|
|
|
import logger from './logger';
|
2026-05-18 21:18:16 +07:00
|
|
|
import { enqueueUpload } from './telegramQueue';
|
2026-05-18 07:06:21 +07:00
|
|
|
|
2026-05-18 21:18:16 +07:00
|
|
|
const botTokens = Array.from(new Set([config.botToken, ...config.additionalBotTokens]));
|
|
|
|
|
|
|
|
|
|
const bots = botTokens.map((token) => new Telegraf(token));
|
2026-05-18 07:06:21 +07:00
|
|
|
const TELEGRAM_API_URL = `https://api.telegram.org/bot${config.botToken}/`;
|
|
|
|
|
|
2026-05-18 21:18:16 +07:00
|
|
|
let currentBotIndex = 0;
|
|
|
|
|
|
|
|
|
|
const executeWithBotRetry = async (
|
|
|
|
|
action: (botInstance: Telegraf) => Promise<any>,
|
|
|
|
|
retries = 5,
|
|
|
|
|
attemptedBots = 0,
|
|
|
|
|
): Promise<any> => {
|
|
|
|
|
const currentBot = bots[currentBotIndex];
|
2026-05-18 21:02:38 +07:00
|
|
|
try {
|
2026-05-18 21:18:16 +07:00
|
|
|
return await action(currentBot);
|
2026-05-18 21:02:38 +07:00
|
|
|
} catch (error: any) {
|
|
|
|
|
const errorStr = error.message || String(error);
|
|
|
|
|
const match = errorStr.match(/retry after (\d+)/i);
|
2026-05-18 21:18:16 +07:00
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
|
// 429 rate limit hit! Rotate bot index instantly
|
|
|
|
|
const prevIndex = currentBotIndex;
|
|
|
|
|
currentBotIndex = (currentBotIndex + 1) % bots.length;
|
|
|
|
|
const nextIndex = currentBotIndex;
|
|
|
|
|
attemptedBots++;
|
|
|
|
|
|
|
|
|
|
if (attemptedBots < bots.length) {
|
|
|
|
|
logger.info(`Bot Index ${prevIndex} hit 429. Instantly rotating to Bot Index ${nextIndex}...`);
|
|
|
|
|
return executeWithBotRetry(action, retries, attemptedBots);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If all bots in the pool have been tried and hit 429, sleep
|
|
|
|
|
if (retries > 0) {
|
|
|
|
|
const seconds = parseInt(match[1], 10);
|
|
|
|
|
logger.warn(`All bots in the pool are rate-limited. Sleeping for ${seconds} seconds...`, {
|
|
|
|
|
error: errorStr,
|
|
|
|
|
});
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
|
|
|
|
return executeWithBotRetry(action, retries - 1, 0);
|
|
|
|
|
}
|
2026-05-18 21:02:38 +07:00
|
|
|
}
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-18 07:27:06 +07:00
|
|
|
interface ForwardResult {
|
|
|
|
|
telegramFileId: string;
|
|
|
|
|
telegramFileUniqueId: string;
|
|
|
|
|
storageMessageId: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface TelegramFileInfo {
|
|
|
|
|
file_size: number;
|
|
|
|
|
mime_type: string;
|
|
|
|
|
file_path: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const forwardToStorage = async (
|
|
|
|
|
fileChunk: any,
|
|
|
|
|
fileName: string,
|
2026-05-18 19:52:46 +07:00
|
|
|
fileType: string,
|
2026-05-18 07:27:06 +07:00
|
|
|
): Promise<ForwardResult> => {
|
2026-05-18 07:06:21 +07:00
|
|
|
try {
|
2026-05-18 21:18:16 +07:00
|
|
|
const result: any = await enqueueUpload(async () => {
|
|
|
|
|
const filePayload = { source: fileChunk, filename: fileName };
|
|
|
|
|
const uploadResult = await executeWithBotRetry((activeBot) => {
|
|
|
|
|
if (fileType === 'photo') {
|
|
|
|
|
return activeBot.telegram.sendPhoto(config.storageChatId, filePayload, {
|
|
|
|
|
caption: fileName,
|
|
|
|
|
});
|
|
|
|
|
} else if (fileType === 'audio') {
|
|
|
|
|
return activeBot.telegram.sendAudio(config.storageChatId, filePayload, {
|
|
|
|
|
caption: fileName,
|
|
|
|
|
});
|
|
|
|
|
} else if (fileType === 'video') {
|
|
|
|
|
return activeBot.telegram.sendVideo(config.storageChatId, filePayload, {
|
|
|
|
|
caption: fileName,
|
|
|
|
|
});
|
|
|
|
|
} else if (fileType === 'voice') {
|
|
|
|
|
return activeBot.telegram.sendVoice(config.storageChatId, filePayload, {
|
|
|
|
|
caption: fileName,
|
|
|
|
|
});
|
|
|
|
|
} else if (fileType === 'animation') {
|
|
|
|
|
return activeBot.telegram.sendAnimation(config.storageChatId, filePayload, {
|
|
|
|
|
caption: fileName,
|
|
|
|
|
});
|
|
|
|
|
} else if (fileType === 'sticker') {
|
|
|
|
|
return activeBot.telegram.sendSticker(config.storageChatId, filePayload);
|
|
|
|
|
} else {
|
|
|
|
|
return activeBot.telegram.sendDocument(config.storageChatId, filePayload, {
|
|
|
|
|
caption: `📁 ${fileName}`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-05-18 19:52:46 +07:00
|
|
|
|
2026-05-18 21:18:16 +07:00
|
|
|
// Advance round-robin index for next job
|
|
|
|
|
currentBotIndex = (currentBotIndex + 1) % bots.length;
|
|
|
|
|
|
|
|
|
|
return uploadResult;
|
|
|
|
|
});
|
2026-05-18 19:52:46 +07:00
|
|
|
|
|
|
|
|
let uploadedFile: any;
|
|
|
|
|
if (result.document) uploadedFile = result.document;
|
|
|
|
|
else if (result.photo) uploadedFile = result.photo?.slice(-1)[0];
|
|
|
|
|
else if (result.video) uploadedFile = result.video;
|
|
|
|
|
else if (result.audio) uploadedFile = result.audio;
|
|
|
|
|
else if (result.voice) uploadedFile = result.voice;
|
|
|
|
|
else if (result.animation) uploadedFile = result.animation;
|
|
|
|
|
else if (result.sticker) uploadedFile = result.sticker;
|
|
|
|
|
else if (result.video_note) uploadedFile = result.video_note;
|
|
|
|
|
else uploadedFile = result[fileType];
|
2026-05-18 07:06:21 +07:00
|
|
|
|
|
|
|
|
logger.info('File forwarded to storage', { fileName, message: result.message_id });
|
|
|
|
|
|
|
|
|
|
return {
|
2026-05-18 08:18:55 +07:00
|
|
|
telegramFileId: uploadedFile?.file_id || '',
|
|
|
|
|
telegramFileUniqueId: uploadedFile?.file_unique_id || '',
|
2026-05-18 07:29:01 +07:00
|
|
|
storageMessageId: result.message_id,
|
2026-05-18 07:06:21 +07:00
|
|
|
};
|
2026-05-18 07:27:06 +07:00
|
|
|
} catch (error: any) {
|
2026-05-18 07:06:21 +07:00
|
|
|
logger.error('Failed to forward file to storage', { fileName, error: error.message });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-18 07:27:06 +07:00
|
|
|
export const getFileInfo = async (
|
|
|
|
|
telegramFileId: string,
|
2026-05-18 07:29:01 +07:00
|
|
|
telegramFileUniqueId: string,
|
2026-05-18 07:27:06 +07:00
|
|
|
): Promise<TelegramFileInfo> => {
|
2026-05-18 07:06:21 +07:00
|
|
|
try {
|
|
|
|
|
const result = await fetch(`${TELEGRAM_API_URL}getFile`);
|
2026-05-18 07:27:06 +07:00
|
|
|
const data: any = await result.json();
|
2026-05-18 07:06:21 +07:00
|
|
|
|
|
|
|
|
if (!data.ok) {
|
|
|
|
|
throw new Error(data.description || 'Telegram API error');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fileId = data.result.file_id === telegramFileId ? telegramFileId : telegramFileUniqueId;
|
|
|
|
|
const fileResult = await fetch(`${TELEGRAM_API_URL}getInfo`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
2026-05-18 07:29:01 +07:00
|
|
|
body: JSON.stringify({ file_id: fileId }),
|
2026-05-18 07:06:21 +07:00
|
|
|
});
|
2026-05-18 07:27:06 +07:00
|
|
|
const fileInfo: any = await fileResult.json();
|
2026-05-18 07:06:21 +07:00
|
|
|
|
|
|
|
|
if (!fileInfo.ok) {
|
|
|
|
|
throw new Error(fileInfo.description || 'Telegram info error');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
file_size: fileInfo.result.file_size,
|
|
|
|
|
mime_type: fileInfo.result.mime_type,
|
2026-05-18 07:29:01 +07:00
|
|
|
file_path: fileInfo.result.file_path,
|
2026-05-18 07:06:21 +07:00
|
|
|
};
|
2026-05-18 07:27:06 +07:00
|
|
|
} catch (error: any) {
|
2026-05-18 07:06:21 +07:00
|
|
|
logger.error('Failed to get file info', { error: error.message });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-18 21:18:16 +07:00
|
|
|
export const getBot = (): Telegraf => bots[0];
|