109 lines
3.8 KiB
JavaScript
109 lines
3.8 KiB
JavaScript
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 <path>\n/put (send file with caption: /put <path>)\n/mv <src> <dst>\n/mkdir <path>\n/rm <path>",
|
|
),
|
|
);
|
|
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 <path>");
|
|
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 <src> <dst>");
|
|
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 <path>");
|
|
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 <path>");
|
|
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"));
|