require("dotenv").config(); const { Telegraf } = require("telegraf"); const fs = require("fs").promises; const path = require("path"); const stream = require("stream"); const { pipeline } = require("stream/promises"); const BOT_TOKEN = process.env.BOT_TOKEN; const BASE_DIR = process.env.BASE_DIR || "/srv/files"; const ALLOWED = (process.env.ALLOWED_TELEGRAM_IDS || "") .split(",") .filter(Boolean) .map((x) => Number(x)); if (!BOT_TOKEN) { console.error("Missing BOT_TOKEN in .env"); process.exit(1); } const bot = new Telegraf(BOT_TOKEN); function allowed(ctx) { const id = ctx.from && ctx.from.id; if (!id) return false; if (ALLOWED.length === 0) return true; return ALLOWED.includes(id); } function safeJoin(base, target) { const resolved = path.resolve(base, "." + path.sep + target); if (!resolved.startsWith(path.resolve(base))) throw new Error("Access denied"); return resolved; } bot.use(async (ctx, next) => { try { if (!allowed(ctx)) return ctx.reply("Access denied"); return await next(); } catch (err) { console.error(err); try { await ctx.reply("Error: " + String(err.message)); } catch (e) {} } }); bot.start((ctx) => ctx.reply("File manager bot ready. Use /help to list commands"), ); bot.help((ctx) => ctx.reply( "/ls [path]\n/get \n/put (send file with caption: /put )\n/mv \n/mkdir \n/rm ", ), ); bot.command("ls", async (ctx) => { const arg = ctx.message.text.split(" ").slice(1).join(" ") || "."; const dir = safeJoin(BASE_DIR, arg); const entries = await fs.readdir(dir, { withFileTypes: true }); const lines = entries.map((e) => `${e.isDirectory() ? "d" : "-"}\t${e.name}`); await ctx.replyWithMarkdownV2("`" + lines.join("\n") + "`"); }); bot.command("mkdir", async (ctx) => { const arg = ctx.message.text.split(" ").slice(1).join(" "); if (!arg) return ctx.reply("Usage: /mkdir "); const target = safeJoin(BASE_DIR, arg); await fs.mkdir(target, { recursive: true }); await ctx.reply("Created " + arg); }); bot.command("mv", async (ctx) => { const parts = ctx.message.text.split(" ").slice(1); if (parts.length < 2) return ctx.reply("Usage: /mv "); const src = safeJoin(BASE_DIR, parts[0]); const dst = safeJoin(BASE_DIR, parts[1]); await fs.rename(src, dst); await ctx.reply("Moved"); }); bot.command("rm", async (ctx) => { const arg = ctx.message.text.split(" ").slice(1).join(" "); if (!arg) return ctx.reply("Usage: /rm "); const target = safeJoin(BASE_DIR, arg); const stat = await fs.lstat(target); if (stat.isDirectory()) await fs.rmdir(target, { recursive: true }); else await fs.unlink(target); await ctx.reply("Removed " + arg); }); bot.command("get", async (ctx) => { const arg = ctx.message.text.split(" ").slice(1).join(" "); if (!arg) return ctx.reply("Usage: /get "); const target = safeJoin(BASE_DIR, arg); await ctx.replyWithDocument({ source: target, filename: path.basename(target), }); }); bot.on("document", async (ctx) => { const caption = (ctx.message.caption || "").trim(); let destPath = "."; if (caption.startsWith("/put")) destPath = caption.split(" ").slice(1).join(" ") || "."; const fileId = ctx.message.document.file_id; const fileName = ctx.message.document.file_name || "file"; const filePath = safeJoin(BASE_DIR, path.join(destPath, fileName)); const fileLink = await ctx.telegram.getFileLink(fileId); const res = await fetch(fileLink.href); if (!res.ok) throw new Error("download failed"); await fs.mkdir(path.dirname(filePath), { recursive: true }); const dest = await fs.open(filePath, "w"); await pipeline(res.body, dest.createWriteStream()); await dest.close(); await ctx.reply("Saved to " + path.relative(BASE_DIR, filePath)); }); bot.launch(); process.once("SIGINT", () => bot.stop("SIGINT")); process.once("SIGTERM", () => bot.stop("SIGTERM"));