# Telegram Bot Uploader Backend Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Production-ready backend for Telegram file uploader with dual upload methods (bot + HTTP API), PostgreSQL storage, and redirect-based downloads. **Architecture:** Bun.serve HTTP server + Telegraf bot + Drizzle ORM, running simultaneously with in-memory rate limiting and graceful shutdown. **Tech Stack:** Bun runtime, Telegraf, PostgreSQL, Drizzle ORM, Winston, nanoid, ESM JavaScript --- ## File Structure ``` src/ index.js — entry point, start server & bot, graceful shutdown bot.js — Telegraf bot setup, file handlers, forward to channel env.js — validate bot_token, storage_channel_id, base_url, database_url, port db/ index.js — Drizzle setup, connection pool schema.js — Drizzle schema definition for files table routes/ files.js — GET /f/:public_id, GET /file/:public_id/info upload.js — POST /api/upload (multipart & base64) health.js — GET /health utils/ file.js — file type validation, size checks against Telegram limits telegram.js — forward to channel, getFile API calls logger.js — Winston logger setup rateLimit.js — in-memory rate limiter (30 req/min per IP) package.json .env.example schema.sql README.md — setup instructions, deploy guide ``` --- ## Task 1: Setup Dependencies and Configuration **Files:** - Create: `package.json` - Create: `.env.example` - Create: `schema.sql` - [ ] **Step 1: Create package.json** ```json { "name": "teleuploader", "version": "1.0.0", "description": "Telegram file uploader backend", "type": "module", "scripts": { "dev": "bun --hot index.js", "start": "NODE_ENV=production bun index.js", "db:migrate": "psql $DATABASE_URL -f schema.sql" }, "dependencies": { "drizzle-orm": "^0.29.0", "telegraf": "^4.15.0", "winston": "^3.11.0", "nanoid": "^5.0.4", "@prisma/engines": "npm:prisma@5.8.0-standalone" }, "devDependencies": { "@types/bun": "latest" } } ``` - [ ] **Step 2: Create .env.example** ```bash BOT_TOKEN=isi_token_bot_telegram STORAGE_CHANNEL_ID=-1001234567890 BASE_URL=https://tele.asepharyana.my.id DATABASE_URL=postgresql://user:password@localhost:5432/telegram_uploader PORT=3000 NODE_ENV=production LOG_LEVEL=info RATE_LIMIT_WINDOW_MS=60000 RATE_LIMIT_MAX_REQUESTS=30 ``` - [ ] **Step 3: Create schema.sql** ```sql CREATE TABLE files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), public_id VARCHAR(21) UNIQUE NOT NULL, telegram_file_id VARCHAR NOT NULL, telegram_file_unique_id VARCHAR NOT NULL, storage_chat_id BIGINT NOT NULL, storage_message_id BIGINT NOT NULL, file_name VARCHAR NOT NULL, mime_type VARCHAR NOT NULL, size_bytes BIGINT NOT NULL, file_type VARCHAR NOT NULL, uploader_id BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_files_public_id ON files(public_id); CREATE INDEX idx_files_telegram_file_id ON files(telegram_file_id); CREATE INDEX idx_files_uploader_id ON files(uploader_id); CREATE INDEX idx_files_created_at ON files(created_at DESC); ``` - [ ] **Step 4: Create README.md** ```markdown # Telegram Bot Uploader Backend Backend production-ready untuk upload file ke Telegram yang tersimpan di private channel. ## Setup 1. Install PostgreSQL database 2. Buat database: `createdb telegram_uploader` 3. Setup environment: `cp .env.example .env` 4. Edit `.env` dengan nilai yang sesuai 5. Create table: `bun run db:migrate` 6. Install dependencies: `bun install` ## Telegram Private Channel Setup 1. Buat private channel Telegram 2. Tambah bot sebagai admin di channel 3. Dapatkan `STORAGE_CHANNEL_ID` (misalnya -1001234567890) ## Running ```bash bun run dev # Development mode bun run start # Production mode ``` ## API Endpoints - `POST /api/upload` - Upload file - `GET /f/:public_id` - Download redirect - `GET /file/:public_id/info` - File metadata - `GET /health` - Health check ## FAQ **URL permanen maksudnya apa?** URL backend tetap permanen: `https://tele.asepharyana.my.id/f/{public_id}` Ini berarti URL service Anda fix, bukan jaminan file Telegram abadi. ## Testing Gunakan bot Telegram untuk upload, atau upload API langsung via HTTP. ``` - [ ] **Step 5: Commit** ```bash git add package.json .env.example schema.sql README.md git commit -m "chore: add dependencies and configuration" ``` --- ## Task 2: Setup Winston Logger **Files:** - Create: `src/utils/logger.js` - [ ] **Step 1: Create logger.js** ```javascript import winston from 'winston'; const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), defaultMeta: { service: 'teleuploader' }, transports: [ // Write all logs including error logs to file new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), new winston.transports.File({ filename: 'logs/combined.log' }) ] }); // If not production, also log to console if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ) })); } export default logger; ``` - [ ] **Step 2: Create logs directory** ```bash mkdir -p logs ``` - [ ] **Step 3: Commit** ```bash git add src/utils/logger.js logs git commit -m "feat: add Winston logger setup" ``` --- ## Task 3: Setup Environment Variables Validation **Files:** - Create: `src/env.js` - [ ] **Step 1: Create env.js** ```javascript import logger from './utils/logger.js'; const requiredEnv = { BOT_TOKEN: process.env.BOT_TOKEN, STORAGE_CHANNEL_ID: process.env.STORAGE_CHANNEL_ID, BASE_URL: process.env.BASE_URL, DATABASE_URL: process.env.DATABASE_URL, PORT: process.env.PORT }; const missing = Object.entries(requiredEnv) .filter(([_, value]) => value === undefined || value === '') .map(([key]) => key); if (missing.length > 0) { logger.error('Missing required environment variables:', missing); throw new Error(`Missing environment variables: ${missing.join(', ')}`); } export const config = { botToken: process.env.BOT_TOKEN, storageChatId: parseInt(process.env.STORAGE_CHANNEL_ID, 10), baseUrl: process.env.BASE_URL, databaseUrl: process.env.DATABASE_URL, port: parseInt(process.env.PORT, 10) || 3000, nodeEnv: process.env.NODE_ENV || 'development', logLevel: process.env.LOG_LEVEL || 'info', rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 60000, rateLimitMaxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10) || 30 }; logger.info('Environment variables loaded', { config: { ...config, botToken: config.botToken?.substring(0, 10) + '...' } }); ``` - [ ] **Step 2: Commit** ```bash git add src/env.js git commit -m "feat: add environment variables validation" ``` --- ## Task 4: Setup Drizzle ORM Database Layer **Files:** - Create: `src/db/index.js` - Create: `src/db/schema.js` - [ ] **Step 1: Install Drizzle dependencies** ```bash bun install drizzle-orm @prisma/engines bun install -D drizzle-kit ``` - [ ] **Step 2: Create schema.js** ```javascript import { sqliteTable, text, bigint, timestamp } from 'drizzle-orm/pg-core'; export const files = sqliteTable('files', { id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), publicId: text('public_id').unique().notNull(), telegramFileId: text('telegram_file_id').notNull(), telegramFileUniqueId: text('telegram_file_unique_id').notNull(), storageChatId: bigint('storage_chat_id', { mode: 'number' }).notNull(), storageMessageId: bigint('storage_message_id', { mode: 'number' }).notNull(), fileName: text('file_name').notNull(), mimeType: text('mime_type').notNull(), sizeBytes: bigint('size_bytes', { mode: 'number' }).notNull(), fileType: text('file_type').notNull(), uploaderId: bigint('uploader_id', { mode: 'number' }).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull() }); export type File = typeof files.$inferSelect; export type NewFile = typeof files.$inferInsert; ``` - [ ] **Step 3: Create index.js** ```javascript import { drizzle } from 'drizzle-orm/postgres-js'; import postgres from 'postgres'; import logger from '../utils/logger.js'; import { files } from './schema.js'; const client = postgres(process.env.DATABASE_URL, { max: 10, idle_timeout: 20, connect_timeout: 10 }); export const db = drizzle(client, { schema: { files } }); export { files }; export default db; ``` - [ ] **Step 4: Commit** ```bash git add src/db/index.js src/db/schema.js git commit -m "feat: add Drizzle ORM database layer" ``` --- ## Task 5: Setup Rate Limiter **Files:** - Create: `src/utils/rateLimit.js` - [ ] **Step 1: Create rateLimit.js** ```javascript import logger from './logger.js'; const rateLimitMap = new Map(); export const checkRateLimit = (key) => { const now = Date.now(); const windowMs = parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 60000; const maxRequests = parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10) || 30; if (!rateLimitMap.has(key)) { rateLimitMap.set(key, { count: 0, reset: now + windowMs }); } const record = rateLimitMap.get(key); if (now > record.reset) { record.count = 0; record.reset = now + windowMs; } if (record.count >= maxRequests) { logger.warn('Rate limit exceeded', { key, count: record.count, reset: record.reset }); return false; } record.count++; return true; }; export const cleanupRateLimitCache = () => { const now = Date.now(); const windowMs = parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 60000; const keysToDelete = []; for (const [key, record] of rateLimitMap.entries()) { if (now > record.reset) { keysToDelete.push(key); } } keysToDelete.forEach(key => rateLimitMap.delete(key)); }; ``` - [ ] **Step 2: Commit** ```bash git add src/utils/rateLimit.js git commit -m "feat: add in-memory rate limiter" ``` --- ## Task 6: Setup File Utility Functions **Files:** - Create: `src/utils/file.js` - [ ] **Step 1: Create file.js** ```javascript import logger from './logger.js'; const FILE_TYPES = { document: 2 * 1024 * 1024 * 1024, // 2GB photo: 10 * 1024 * 1024, // 10MB video: 2 * 1024 * 1024 * 1024, // 2GB audio: 200 * 1024 * 1024, // 200MB voice: 200 * 1024 * 1024, // 200MB animation: 2 * 1024 * 1024 * 1024 // 2GB }; export const getFileType = (mime, caption) => { const mimeUpper = mime?.split('/')[0]?.toLowerCase(); const captionLower = caption?.toLowerCase(); if (mimeUpper === 'video') return 'video'; if (mimeUpper === 'audio') return 'audio'; if (mimeUpper === 'document') return 'document'; if (mimeUpper === 'image') return captionLower?.includes('gif') ? 'animation' : 'photo'; if (captionLower?.includes('voice')) return 'voice'; if (captionLower?.includes('animation')) return 'animation'; return mimeUpper || 'document'; }; export const checkFileSize = (sizeBytes, fileType) => { const limit = FILE_TYPES[fileType] || FILE_TYPES.document; return sizeBytes <= limit; }; export const extractFileName = (msg, request) => { if (request?.headers?.['x-file-name']) { return request.headers['x-file-name']; } return msg.document?.fileName || msg.photo?.slice(-1)[0]?.fileName || msg.audio?.fileName || msg.voice?.fileName || msg.animation?.fileName || 'file'; }; export const extractMimeType = (msg, request) => { if (request?.headers?.['x-mime-type']) { return request.headers['x-mime-type']; } return msg.document?.mimeType || msg.photo?.slice(-1)[0]?.mimeType || msg.audio?.mimeType || msg.voice?.mimeType || msg.animation?.mimeType || 'application/octet-stream'; }; ``` - [ ] **Step 2: Commit** ```bash git add src/utils/file.js git commit -m "feat: add file validation utilities" ``` --- ## Task 7: Setup Telegram API Utilities **Files:** - Create: `src/utils/telegram.js` - [ ] **Step 1: Create telegram.js** ```javascript 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; ``` - [ ] **Step 2: Commit** ```bash git add src/utils/telegram.js git commit -m "feat: add Telegram API utilities" ``` --- ## Task 8: Setup Upload API Handler **Files:** - Create: `src/routes/upload.js` - [ ] **Step 1: Create upload.js** ```javascript import { Request, Response } from 'bun'; import logger from '../utils/logger.js'; import { db, files as fileSchema } from '../db/index.js'; import { nanoid } from 'nanoid'; import { forwardToStorage, getBot } from '../utils/telegram.js'; import { getFileType, checkFileSize, extractFileName, extractMimeType } from '../utils/file.js'; import { config } from '../env.js'; export const handleUpload = async (req, res) => { try { const contentType = req.headers.get('content-type') || ''; if (contentType.includes('multipart/form-data')) { return handleMultipartUpload(req, res); } else if (contentType.includes('application/json')) { return handleJSONUpload(req, res); } return res.status(400).json({ error: 'Unsupported content type. Use multipart/form-data or application/json' }); } catch (error) { logger.error('Upload error', { error: error.message }); return res.status(500).json({ error: error.message }); } }; const handleMultipartUpload = async (req, res) => { try { const formData = await req.formData(); const file = formData.get('file'); const fileName = formData.get('fileName') || extractFileName({}, null); if (!file || !(file instanceof File)) { return res.status(400).json({ error: 'No file provided' }); } const fileBytes = await file.arrayBuffer(); const fileBuffer = Buffer.from(fileBytes); const fileType = getFileType(null, fileName); if (!checkFileSize(fileBuffer.byteLength, fileType)) { return res.status(400).json({ error: `File size exceeds ${fileType} limit` }); } const uploadTelegram = async (forceDocument = false) => { const result = await forwardToStorage(fileBuffer, fileName, forceDocument || fileName.endsWith('.pdf') || fileName.endsWith('.txt')); const bot = getBot(); const fileInfo = await bot.api.getFile(result.telegramFileId); return { public_id: nanoid(), telegram_file_id: result.telegramFileId, telegram_file_unique_id: result.telegramFileUniqueId, storage_chat_id: config.storageChatId, storage_message_id: result.storageMessageId, file_name: fileName, mime_type: fileInfo.mime_type || 'application/octet-stream', size_bytes: fileInfo.file_size, file_type: fileType, uploader_id: 0, created_at: new Date().toISOString(), download_url: `${config.baseUrl}/f/${nanoid()}` }; }; const uploaded = await uploadTelegram(fileName.endsWith('.pdf') || fileName.endsWith('.txt') || !['photo', 'video', 'audio', 'voice', 'animation'].includes(fileType)); await db.insert(fileSchema).values(uploaded); res.status(200).json(uploaded); } catch (error) { logger.error('Multipart upload error', { error: error.message }); res.status(500).json({ error: error.message }); } }; const handleJSONUpload = async (req, res) => { try { const { file, fileName = 'file' } = await req.json(); if (!file || typeof file !== 'string') { return res.status(400).json({ error: 'Invalid JSON. Must include "file" (base64) and optional "fileName"' }); } const fileBytes = Buffer.from(file, 'base64'); const fileType = getFileType(null, fileName); if (!checkFileSize(fileBytes.byteLength, fileType)) { return res.status(400).json({ error: `File size exceeds ${fileType} limit` }); } const result = await forwardToStorage(fileBytes, fileName); const bot = getBot(); const fileInfo = await bot.api.getFile(result.telegramFileId); const uploaded = { public_id: nanoid(), telegram_file_id: result.telegramFileId, telegram_file_unique_id: result.telegramFileUniqueId, storage_chat_id: config.storageChatId, storage_message_id: result.storageMessageId, file_name: fileName, mime_type: fileInfo.mime_type || 'application/octet-stream', size_bytes: fileInfo.file_size, file_type: fileType, uploader_id: 0, created_at: new Date().toISOString(), download_url: `${config.baseUrl}/f/${nanoid()}` }; await db.insert(fileSchema).values(uploaded); res.status(200).json(uploaded); } catch (error) { logger.error('JSON upload error', { error: error.message }); res.status(500).json({ error: error.message }); } }; ``` - [ ] **Step 2: Commit** ```bash git add src/routes/upload.js git commit -m "feat: add upload API handler (multipart & base64)" ``` --- ## Task 9: Setup File Routes **Files:** - Create: `src/routes/files.js` - [ ] **Step 1: Create files.js** ```javascript import { Request, Response } from 'bun'; import logger from '../utils/logger.js'; import { db, files as fileSchema } from '../db/index.js'; import { checkRateLimit } from '../utils/rateLimit.js'; export const handleFileRedirect = async (req, res) => { try { const { public_id } = req.params; if (!public_id || !checkRateLimit(req.ip)) { return res.status(429).json({ error: 'Rate limit exceeded' }); } const result = await db.select().from(fileSchema).where(fileSchema.public_id.equals(public_id)).limit(1); if (!result.length) { logger.warn('File not found', { public_id }); return res.status(404).json({ error: 'File not found' }); } const file = result[0]; const bot = (await import('../utils/telegram.js')).getBot(); const fileInfo = await bot.api.getFile(file.telegram_file_id); const redirectUrl = `https://api.telegram.org/file/bot${process.env.BOT_TOKEN}/${fileInfo.file_path}`; res.status(302).headers.set('Location', redirectUrl); } catch (error) { logger.error('File redirect error', { public_id, error: error.message }); res.status(500).json({ error: 'Server error' }); } }; export const handleFileInfo = async (req, res) => { try { const { public_id } = req.params; const result = await db.select().from(fileSchema).where(fileSchema.public_id.equals(public_id)).limit(1); if (!result.length) { logger.warn('File not found', { public_id }); return res.status(404).json({ error: 'File not found' }); } const file = result[0]; res.status(200).json({ public_id: file.public_id, file_name: file.file_name, mime_type: file.mime_type, size_bytes: file.size_bytes, file_type: file.file_type, uploader_id: file.uploader_id, created_at: file.created_at }); } catch (error) { logger.error('File info error', { public_id, error: error.message }); res.status(500).json({ error: 'Server error' }); } }; ``` - [ ] **Step 2: Commit** ```bash git add src/routes/files.js git commit -m "feat: add file routes (download redirect & info)" ``` --- ## Task 10: Setup Health Route **Files:** - Create: `src/routes/health.js` - [ ] **Step 1: Create health.js** ```javascript import { Response } from 'bun'; import logger from '../utils/logger.js'; import { db } from '../db/index.js'; export const handleHealth = async (req, res) => { try { await db.execute('SELECT 1'); res.status(200).json({ status: 'ok' }); } catch (error) { logger.error('Health check failed', { error: error.message }); res.status(500).json({ status: 'error', error: error.message }); } }; ``` - [ ] **Step 2: Commit** ```bash git add src/routes/health.js git commit -m "feat: add health check route" ``` --- ## Task 11: Setup Telegram Bot Handler **Files:** - Create: `src/bot.js` - [ ] **Step 1: Create bot.js** ```javascript import { Telegraf } from 'telegraf'; import logger from './utils/logger.js'; import { config } from './env.js'; import { db, files as fileSchema } from './db/index.js'; import { nanoid } from 'nanoid'; import { forwardToStorage } from './utils/telegram.js'; export const startBot = async () => { 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. ` + `File akan disimpan di private channel dan kamu dapat download link permanen.` ); }); bot.on(['document', 'photo', 'video', 'audio', 'voice', 'animation'], async (ctx) => { try { const fileType = ctx.message.document ? 'document' : ctx.message.photo ? 'photo' : ctx.message.video ? 'video' : ctx.message.audio ? 'audio' : ctx.message.voice ? 'voice' : 'animation'; const { file_id, file_unique_id, file_size, mime_type } = ctx.message[fileType]; const fileName = ctx.message.document?.fileName || ctx.message.photo?.slice(-1)[0]?.fileName || ctx.message.video?.fileName || ctx.message.audio?.fileName || ctx.message.voice?.fileName || 'file'; const maxSize = fileType === 'photo' ? 10 * 1024 * 1024 : fileType === 'audio' ? 200 * 1024 * 1024 : fileType === 'voice' ? 200 * 1024 * 1024 : 2 * 1024 * 1024 * 1024; if (file_size > maxSize) { return ctx.reply(`File size exceeds ${maxSize / (1024 * 1024)}MB limit`); } const result = await forwardToStorage(file_id, fileName); const publicId = nanoid(); const uploaded = { public_id: publicId, telegram_file_id: result.telegramFileId, telegram_file_unique_id: result.telegramFileUniqueId, storage_chat_id: config.storageChatId, storage_message_id: result.storageMessageId, file_name: fileName, mime_type: mime_type, size_bytes: file_size, file_type: fileType, uploader_id: ctx.from.id, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; await db.insert(fileSchema).values(uploaded); const url = `${config.baseUrl}/f/${publicId}`; await ctx.reply(`File berhasil diupload! 📎\n\nDownload: ${url}`, { reply_parameters: { message_id: ctx.message.message_id } }); logger.info('File uploaded via bot', { public_id, fileType, fileName, uploader: ctx.from.id }); } catch (error) { logger.error('Bot file handler error', { error: error.message, chat_id: ctx.chat.id }); await ctx.reply('❌ Gagal mengupload file. Coba lagi nanti.'); } }); bot.use((ctx, next) => { logger.info('Telegram event received', { type: ctx.update.type, chat_id: ctx.chat?.id }); return next(); }); await bot.launch(); logger.info('Telegram bot started', { botToken: config.botToken?.substring(0, 10) + '...' }); return bot; } catch (error) { logger.error('Failed to start bot', { error: error.message }); throw error; } }; ``` - [ ] **Step 2: Commit** ```bash git add src/bot.js git commit -m "feat: add Telegram bot handler" ``` --- ## Task 12: Setup HTTP Server and Bootstrap **Files:** - Create: `src/index.js` - [ ] **Step 1: Create index.js** ```javascript import { serve } from 'bun'; import logger from './utils/logger.js'; import { config } from './env.js'; import { startBot } from './bot.js'; import { handleUpload } from './routes/upload.js'; import { handleFileRedirect, handleFileInfo } from './routes/files.js'; import { handleHealth } from './routes/health.js'; import { cleanupRateLimitCache } from './utils/rateLimit.js'; const server = serve({ port: config.port, routes: { '/api/upload': { POST: handleUpload }, '/f/:public_id': { GET: handleFileRedirect }, '/file/:public_id/info': { GET: handleFileInfo }, '/health': { GET: handleHealth } } }); const bot = await startBot(); logger.info('Server started', { port: config.port, url: config.baseUrl }); const gracefulShutdown = async (signal) => { logger.info('Graceful shutdown signal received', { signal }); logger.info('Closing HTTP server'); server.stop(); logger.info('Stopping Telegram bot'); await bot.stop(); logger.info('Server shutdown complete'); process.exit(0); }; process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); setInterval(cleanupRateLimitCache, 60000); logger.info('Application running successfully'); ``` - [ ] **Step 2: Commit** ```bash git add src/index.js git commit -m "feat: setup HTTP server and bootstrap application" ``` --- ## Task 13: Update Existing Files **Files:** - Modify: `package.json` - Modify: `index.ts` - Modify: `tsconfig.json` - [ ] **Step 1: Remove TypeScript files** ```bash rm index.ts tsconfig.json ``` - [ ] **Step 2: Update package.json to remove TypeScript devDependency** ```bash # Edit package.json to remove lines 6-10 (devDependencies @types/bun and typescript peerDependency) ``` - [ ] **Step 3: Install dependencies** ```bash bun install ``` - [ ] **Step 4: Commit** ```bash git add -A git commit -m "refactor: replace TypeScript with ESM JavaScript" ``` --- ## Task 14: Test and Verify **Commands:** - [ ] **Step 1: Run development server** ```bash bun run dev ``` - [ ] **Step 2: Test health endpoint** ```bash curl http://localhost:3000/health # Expected: {"status":"ok"} ``` - [ ] **Step 3: Upload test file via HTTP API (multipart)** ```bash curl -X POST http://localhost:3000/api/upload \ -F "file=@/path/to/testfile.txt" \ -F "fileName=test.txt" ``` - [ ] **Step 4: Check file info endpoint** ```bash curl http://localhost:3000/file/{public_id}/info # Expected: JSON with file metadata ``` - [ ] **Step 5: Download redirect** ```bash curl -I http://localhost:3000/f/{public_id} # Expected: HTTP 302 with Location header to Telegram CDN ``` - [ ] **Step 6: Test bot upload on Telegram** ``` 1. Start bot: /start 2. Send test file to bot 3. Check if file is forwarded to storage channel 4. Verify response contains download link ``` - [ ] **Step 7: Test error handling (file too large)** ```bash curl -X POST http://localhost:3000/api/upload \ -F "file=@/dev/null" \ -H "Content-Length: 10000000000" # Expected: HTTP 400 with error message ``` - [ ] **Step 8: Test rate limiting** ```bash # Send 31 requests within 1 minute for i in {1..31}; do curl http://localhost:3000/f/{public_id} & done wait # Expected: First 30 succeed, last one returns 429 ``` - [ ] **Step 9: Test graceful shutdown** ```bash # In terminal 1: bun run dev # In terminal 2: curl http://localhost:3000/health && sleep 0.1 && curl http://localhost:3000/health # Send SIGINT to server (Ctrl+C in terminal 1) # Check if server stops cleanly, logs show shutdown sequence ``` - [ ] **Step 10: Review logs** ```bash # Check logs directory ls -la logs/ # Expected: error.log and combined.log created # View error logs: tail -f logs/error.log # View combined logs: tail -f logs/combined.log ``` --- ## Task 15: Production Ready **Files:** - Modify: `.env.example` - Modify: `README.md` - [ ] **Step 1: Update .env.example with production-ready defaults** ```bash # Copy .env.example to .env and update values for production ``` - [ ] **Step 2: Update README.md with production deployment notes** ```bash # Update README.md with: # - PM2 setup for production # - Reverse proxy (Nginx) configuration # - SSL certificate setup # - Database backup strategy ``` - [ ] **Step 3: Commit production-ready changes** ```bash git add .env.example README.md git commit -m "docs: add production deployment guide" ``` --- ## Summary This plan implements a complete production-ready Telegram bot uploader backend with: ✅ Bun runtime with ESM JavaScript ✅ Telegraf bot listening for file uploads ✅ HTTP API accepting multipart and base64 uploads ✅ PostgreSQL storage with Drizzle ORM ✅ Redirect-based downloads (minimal VPS bandwidth) ✅ In-memory rate limiting (30 req/min per IP) ✅ Winston structured logging ✅ Graceful shutdown handling ✅ All required endpoints (health, upload, download, info) ✅ Environment variable validation ✅ Comprehensive error handling **Plan complete and saved to `docs/superpowers/plans/2026-05-17-telegram-uploader-backend-plan.md`** Two execution options: **1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration **2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints **Which approach?**