2026-05-18 07:13:17 +07:00
|
|
|
import { nanoid } from 'nanoid';
|
2026-05-18 07:29:01 +07:00
|
|
|
import { type Context, Telegraf } from 'telegraf';
|
|
|
|
|
import { db, files as fileSchema } from './db';
|
2026-05-21 22:31:55 +07:00
|
|
|
import { findFileByUniqueId } from './db/files';
|
2026-05-18 07:29:01 +07:00
|
|
|
import { config } from './env';
|
2026-05-21 22:31:55 +07:00
|
|
|
import {
|
|
|
|
|
detectFileType,
|
|
|
|
|
extractFileFromMessage,
|
|
|
|
|
getErrorMessage,
|
|
|
|
|
getFileSizeLimit,
|
|
|
|
|
type TelegramMediaMessage,
|
|
|
|
|
} from './utils/file';
|
2026-05-18 07:29:01 +07:00
|
|
|
import logger from './utils/logger';
|
2026-05-18 07:27:06 +07:00
|
|
|
import { forwardToStorage } from './utils/telegram';
|
2026-05-18 07:13:17 +07:00
|
|
|
|
2026-05-21 22:31:55 +07:00
|
|
|
type BotContext = {
|
|
|
|
|
message: TelegramMediaMessage;
|
|
|
|
|
from: { id: number };
|
|
|
|
|
chat?: { id: number };
|
|
|
|
|
reply: (text: string, extra?: { reply_parameters: { message_id: number } }) => Promise<unknown>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type MediaEventRegistrar = {
|
|
|
|
|
on: (events: string[], handler: (ctx: BotContext) => Promise<unknown>) => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const replyWithDownloadUrl = async (ctx: BotContext, publicId: string): Promise<void> => {
|
|
|
|
|
const url = `${config.baseUrl}/f/${publicId}`;
|
|
|
|
|
await ctx.reply(`File berhasil diupload! 📎\n\nDownload: ${url}`, {
|
|
|
|
|
reply_parameters: { message_id: ctx.message.message_id },
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-18 07:27:06 +07:00
|
|
|
export const startBot = async (): Promise<Telegraf<Context>> => {
|
2026-05-18 07:13:17 +07:00
|
|
|
try {
|
|
|
|
|
const bot = new Telegraf(config.botToken);
|
|
|
|
|
|
|
|
|
|
bot.command('start', async (ctx) => {
|
|
|
|
|
await ctx.reply(
|
|
|
|
|
`👋 Halo! Kirimkan file (document, photo, video, audio, voice, animation) ke bot ini. ` +
|
2026-05-18 07:29:01 +07:00
|
|
|
`File akan disimpan di private channel dan kamu dapat download link permanen.`,
|
2026-05-18 07:13:17 +07:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-21 22:31:55 +07:00
|
|
|
const mediaBot = bot as unknown as MediaEventRegistrar;
|
|
|
|
|
mediaBot.on(
|
2026-05-18 19:41:16 +07:00
|
|
|
['document', 'photo', 'video', 'audio', 'voice', 'animation', 'sticker', 'video_note'],
|
2026-05-21 22:31:55 +07:00
|
|
|
async (ctx) => {
|
2026-05-18 07:29:01 +07:00
|
|
|
try {
|
2026-05-21 22:31:55 +07:00
|
|
|
const fileType = detectFileType(ctx.message);
|
|
|
|
|
const fileObj = extractFileFromMessage(ctx.message, fileType);
|
|
|
|
|
const { file_id, mime_type } = fileObj;
|
|
|
|
|
const fileSize = fileObj.file_size || 0;
|
2026-05-18 07:29:01 +07:00
|
|
|
const fileName =
|
|
|
|
|
ctx.message.document?.file_name ||
|
|
|
|
|
ctx.message.photo?.slice(-1)[0]?.file_name ||
|
|
|
|
|
ctx.message.video?.file_name ||
|
|
|
|
|
ctx.message.audio?.file_name ||
|
|
|
|
|
ctx.message.voice?.file_name ||
|
|
|
|
|
'file';
|
2026-05-18 07:13:17 +07:00
|
|
|
|
2026-05-21 22:31:55 +07:00
|
|
|
const maxSize = getFileSizeLimit(fileType);
|
2026-05-18 07:13:17 +07:00
|
|
|
|
2026-05-21 22:31:55 +07:00
|
|
|
if (fileSize > maxSize) {
|
2026-05-18 07:29:01 +07:00
|
|
|
return ctx.reply(`File size exceeds ${maxSize / (1024 * 1024)}MB limit`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 22:31:55 +07:00
|
|
|
const existing = await findFileByUniqueId(fileObj.file_unique_id);
|
2026-05-18 19:41:16 +07:00
|
|
|
|
2026-05-21 22:31:55 +07:00
|
|
|
if (existing) {
|
|
|
|
|
await replyWithDownloadUrl(ctx, existing.publicId);
|
2026-05-18 19:41:16 +07:00
|
|
|
logger.info('Duplicate file detected in bot, returned existing link', {
|
2026-05-21 22:31:55 +07:00
|
|
|
publicId: existing.publicId,
|
2026-05-18 19:41:16 +07:00
|
|
|
fileType,
|
|
|
|
|
fileName,
|
|
|
|
|
uploader: ctx.from.id,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 19:52:46 +07:00
|
|
|
const result = await forwardToStorage(file_id, fileName, fileType);
|
2026-05-18 07:29:01 +07:00
|
|
|
const publicId = nanoid();
|
|
|
|
|
|
|
|
|
|
const uploaded = {
|
|
|
|
|
publicId: publicId,
|
|
|
|
|
telegramFileId: result.telegramFileId,
|
|
|
|
|
telegramFileUniqueId: result.telegramFileUniqueId,
|
|
|
|
|
storageChatId: config.storageChatId,
|
|
|
|
|
storageMessageId: result.storageMessageId,
|
|
|
|
|
fileName: fileName,
|
|
|
|
|
mimeType: mime_type || 'application/octet-stream',
|
2026-05-21 22:31:55 +07:00
|
|
|
sizeBytes: fileSize,
|
2026-05-18 07:29:01 +07:00
|
|
|
fileType: fileType,
|
|
|
|
|
uploaderId: ctx.from.id,
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await db.insert(fileSchema).values(uploaded);
|
|
|
|
|
|
2026-05-21 22:31:55 +07:00
|
|
|
await replyWithDownloadUrl(ctx, publicId);
|
2026-05-18 07:29:01 +07:00
|
|
|
|
|
|
|
|
logger.info('File uploaded via bot', {
|
|
|
|
|
publicId,
|
|
|
|
|
fileType,
|
|
|
|
|
fileName,
|
|
|
|
|
uploader: ctx.from.id,
|
|
|
|
|
});
|
2026-05-21 22:31:55 +07:00
|
|
|
} catch (error: unknown) {
|
|
|
|
|
logger.error('Bot file handler error', {
|
|
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
chat_id: ctx.chat?.id,
|
|
|
|
|
});
|
2026-05-18 07:29:01 +07:00
|
|
|
await ctx.reply('❌ Gagal mengupload file. Coba lagi nanti.');
|
2026-05-18 07:13:17 +07:00
|
|
|
}
|
2026-05-18 07:29:01 +07:00
|
|
|
},
|
|
|
|
|
);
|
2026-05-18 07:13:17 +07:00
|
|
|
|
|
|
|
|
bot.use((ctx, next) => {
|
2026-05-18 07:29:01 +07:00
|
|
|
logger.info('Telegram event received', {
|
2026-05-21 22:31:55 +07:00
|
|
|
type: 'type' in ctx.update ? ctx.update.type : undefined,
|
2026-05-18 07:29:01 +07:00
|
|
|
chat_id: ctx.chat?.id,
|
|
|
|
|
});
|
2026-05-18 07:13:17 +07:00
|
|
|
return next();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await bot.launch();
|
|
|
|
|
|
2026-05-18 07:29:01 +07:00
|
|
|
logger.info('Telegram bot started', { botToken: `${config.botToken?.substring(0, 10)}...` });
|
2026-05-18 07:13:17 +07:00
|
|
|
|
|
|
|
|
return bot;
|
2026-05-21 22:31:55 +07:00
|
|
|
} catch (error: unknown) {
|
|
|
|
|
logger.error('Failed to start bot', { error: getErrorMessage(error) });
|
2026-05-18 07:13:17 +07:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
};
|