From 1e29433d5d6ccf3f3982dce5862b10fa9833bb88 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Mon, 18 May 2026 07:06:21 +0700 Subject: [PATCH] feat: add Telegram API utilities Co-Authored-By: Claude Opus 4.7 --- src/utils/telegram.js | 60 ++++++++++++++++++++ test/telegram.test.js | 127 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 src/utils/telegram.js create mode 100644 test/telegram.test.js diff --git a/src/utils/telegram.js b/src/utils/telegram.js new file mode 100644 index 0000000..33c63ab --- /dev/null +++ b/src/utils/telegram.js @@ -0,0 +1,60 @@ +import { Telegraf } from 'telegraf'; +import logger from './logger.js'; +import { config } from '../env.js'; + +const bot = new Telegraf(config.botToken); +const TELEGRAM_API_URL = `https://api.telegram.org/bot${config.botToken}/`; + +export const forwardToStorage = async (fileChunk, fileName, forceDocument = false) => { + try { + const caption = forceDocument ? `📁 ${fileName}` : fileName; + const input = forceDocument ? { document: fileChunk, caption } : { photo: [fileChunk], caption }; + + const result = await bot.api.sendPhoto(config.storageChatId, input); + + logger.info('File forwarded to storage', { fileName, message: result.message_id }); + + return { + telegramFileId: result.photo?.slice(-1)[0]?.file_id, + telegramFileUniqueId: result.photo?.slice(-1)[0]?.file_unique_id, + storageMessageId: result.message_id + }; + } catch (error) { + logger.error('Failed to forward file to storage', { fileName, error: error.message }); + throw error; + } +}; + +export const getFileInfo = async (telegramFileId, telegramFileUniqueId) => { + try { + const result = await fetch(`${TELEGRAM_API_URL}getFile`); + const data = await result.json(); + + 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' }, + body: JSON.stringify({ file_id: fileId }) + }); + const fileInfo = await fileResult.json(); + + if (!fileInfo.ok) { + throw new Error(fileInfo.description || 'Telegram info error'); + } + + return { + file_size: fileInfo.result.file_size, + mime_type: fileInfo.result.mime_type, + file_path: fileInfo.result.file_path + }; + } catch (error) { + logger.error('Failed to get file info', { error: error.message }); + throw error; + } +}; + +export const getBot = () => bot; diff --git a/test/telegram.test.js b/test/telegram.test.js new file mode 100644 index 0000000..39f9f9f --- /dev/null +++ b/test/telegram.test.js @@ -0,0 +1,127 @@ +import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"; +import logger from "../src/utils/logger.js"; +import { config } from "../src/env.js"; + +// Mock Telegraf and fetch +mock.module("telegraf", () => { + return { + Telegraf: class { + constructor(token) { + this.token = token; + this.api = { + sendPhoto: mock(() => Promise.resolve({ + message_id: 12345, + photo: [ + { file_id: "photo_id_low", file_unique_id: "unique_id_low" }, + { file_id: "photo_id_high", file_unique_id: "unique_id_high" } + ] + })) + }; + } + } + }; +}); + +const infoSpy = spyOn(logger, "info"); +const errorSpy = spyOn(logger, "error"); + +describe("Telegram API Utilities", () => { + let forwardToStorage, getFileInfo, getBot; + + beforeEach(async () => { + infoSpy.mockClear(); + errorSpy.mockClear(); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify({ ok: true })))); + + // Import dynamically so mocking is applied first + const telegramUtils = await import("../src/utils/telegram.js"); + forwardToStorage = telegramUtils.forwardToStorage; + getFileInfo = telegramUtils.getFileInfo; + getBot = telegramUtils.getBot; + }); + + afterEach(() => { + delete global.fetch; + }); + + describe("getBot", () => { + it("should return the telegraf bot instance", () => { + const bot = getBot(); + expect(bot).toBeDefined(); + expect(bot.api).toBeDefined(); + }); + }); + + describe("forwardToStorage", () => { + it("should forward photo to storage chat and return file details", async () => { + const chunk = Buffer.from("fake photo data"); + const fileName = "test_photo.jpg"; + const result = await forwardToStorage(chunk, fileName, false); + + expect(result).toEqual({ + telegramFileId: "photo_id_high", + telegramFileUniqueId: "unique_id_high", + storageMessageId: 12345 + }); + expect(infoSpy).toHaveBeenCalledWith("File forwarded to storage", { + fileName, + message: 12345 + }); + }); + + it("should handle error when forwarding fails", async () => { + const bot = getBot(); + bot.api.sendPhoto = mock(() => Promise.reject(new Error("Telegram send failed"))); + + const chunk = Buffer.from("fake photo data"); + const fileName = "test_photo.jpg"; + + await expect(forwardToStorage(chunk, fileName, false)).rejects.toThrow("Telegram send failed"); + expect(errorSpy).toHaveBeenCalledWith("Failed to forward file to storage", { + fileName, + error: "Telegram send failed" + }); + }); + }); + + describe("getFileInfo", () => { + it("should fetch file details successfully", async () => { + global.fetch = mock((url, init) => { + if (url.endsWith("getFile")) { + return Promise.resolve(new Response(JSON.stringify({ + ok: true, + result: { file_id: "some_file_id" } + }))); + } else if (url.endsWith("getInfo")) { + return Promise.resolve(new Response(JSON.stringify({ + ok: true, + result: { + file_size: 98765, + mime_type: "image/jpeg", + file_path: "photos/file_0.jpg" + } + }))); + } + return Promise.reject(new Error("Unknown URL")); + }); + + const result = await getFileInfo("some_file_id", "some_unique_id"); + + expect(result).toEqual({ + file_size: 98765, + mime_type: "image/jpeg", + file_path: "photos/file_0.jpg" + }); + }); + + it("should handle error when getFile fails", async () => { + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify({ + ok: false, + description: "Bad Request: file_id invalid" + })))); + + await expect(getFileInfo("invalid_file_id", "invalid_unique_id")).rejects.toThrow("Bad Request: file_id invalid"); + expect(errorSpy).toHaveBeenCalled(); + }); + }); +});