feat: add Telegram API utilities
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
b621a73ca9
commit
1e29433d5d
@@ -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;
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user