← back to browse
goat command

file

Browse/manage command files

7
views
0
likes
7
installs

Aliases: files, fm

raw source
const fs = require("fs-extra");
const path = require("path");
const axios = require("axios");

const PASTEBIN_API = "https://pastebin-v2-chi.vercel.app";
const IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".webp"];

// Project root directory
const ROOT_DIR = path.join(__dirname, "..", "..");
// Where command files live relative to project root ("modules/cmds")
const COMMANDS_RELDIR = path.relative(ROOT_DIR, __dirname).split(path.sep).join("/");

// Noise we don't want cluttering the root listing
const IGNORE_NAMES = new Set(["node_modules", ".git", ".env", "package-lock.json"]);

function isImageFile(name) {
    return IMAGE_EXTENSIONS.includes(path.extname(name).toLowerCase());
}

function listDir(dirAbs) {
    const names = fs.readdirSync(dirAbs);
    return names
        .filter(name => !name.startsWith(".") && !IGNORE_NAMES.has(name))
        .map(name => {
            const full = path.join(dirAbs, name);
            const stat = fs.statSync(full);
            return {
                name,
                isDir: stat.isDirectory(),
                isImage: !stat.isDirectory() && isImageFile(name)
            };
        }).sort((a, b) => {
            if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
            return a.name.localeCompare(b.name);
        });
}

function renderList(relDir, entries) {
    const title = relDir ? relDir.split("/").pop() : "root";
    let msg = `🗂️ 『 ${title} 』\n`;
    entries.forEach((e, idx) => {
        const branch = idx === entries.length - 1 ? "└" : "├";
        const icon = e.isDir ? "📂" : (e.isImage ? "🖼️" : "📄");
        msg += `${branch}${idx + 1}. ${icon} ${e.name}\n`;
    });
    if (entries.length === 0) msg += "『 empty 』\n";
    msg += `\n▭▭▭▭▭▭▭▭▭▭▭▭▭▭\n`;
    msg += `✦Reply a number to open\n`;
    if (relDir) {
        msg += `✦Reply "back" ↩️ to go up a level\n`;
    }
    msg += `✦Reply "delete 1,2,3" 🗑️ to delete\n`;
    msg += `✦Reply "raw 1,2,3" 🔗 to get paste links\n`;
    msg += `▭▭▭▭▭▭▭▭▭▭▭▭▭▭`;
    return msg;
}

// Removes the previous "page" message once a new one has been sent
function deleteOldPage(api, Reply) {
    if (!Reply || !Reply.messageID) return;
    try {
        api.unsendMessage(Reply.messageID, () => {});
    } catch (e) {
        // ignore - message may be too old to unsend, or already gone
    }
}

module.exports = {
  config: {
    name: "file",
    aliases: ["files", "fm"],
    version: "2.0.0",
    author: "rX",
    countDown: 5,
    role: 1,
    shortDescription: "Browse/manage command files",
    longDescription: "Browse, view (with image preview), and delete files/folders across the bot's project (commands, events, etc).",
    category: "system",
    guide: {
      en:
        "{pn} — show all files/folders in the commands folder\n" +
        "{pn} start <text> — filter files starting with text\n" +
        "{pn} ext <text> — filter files by extension\n" +
        "{pn} <text> — filter files containing text\n" +
        "{pn} help — show this guide\n" +
        "Reply a number to open a folder or preview an image\n" +
        "Reply \"back\" to go up a level (from commands root, this shows the project root: commands, events, etc.)\n" +
        "Reply \"delete 1,2,3\" to delete\n" +
        "Reply \"raw 1,2,3\" to get paste links for those files"
    }
  },

  onReply: async function ({ api, event, Reply }) {
    if (event.senderID != Reply.author) return;
    const body = (event.body || "").trim();

    if (Reply.stage === "viewFile") {
        if (body.toLowerCase() !== "raw") {
            return api.sendMessage('❌ Reply "raw" to get a paste link, or ignore this message.', event.threadID, event.messageID);
        }
        const abs = path.join(ROOT_DIR, Reply.filePath);
        let code;
        try {
            code = fs.readFileSync(abs, "utf8");
        } catch (e) {
            return api.sendMessage("❌ Couldn't read that file (it may have been deleted).", event.threadID, event.messageID);
        }
        try {
            const res = await axios.post(`${PASTEBIN_API}/api/paste`, { code });
            if (res.data && res.data.url) {
                return api.sendMessage(`📄 File: ${path.basename(Reply.filePath)}\n🔗 ${res.data.url}`, event.threadID, event.messageID);
            }
            return api.sendMessage("⚠️ Upload failed - no valid link received.", event.threadID, event.messageID);
        } catch (e) {
            return api.sendMessage("❌ Upload failed: " + e.message, event.threadID, event.messageID);
        }
    }

    const relDir = Reply.relDir || "";
    const dirAbs = path.join(ROOT_DIR, relDir);
    const entries = Reply.entries;

    if (body.toLowerCase() === "back") {
        if (!relDir) return api.sendMessage("⚡️Already at the project root.", event.threadID, event.messageID);
        const parent = relDir.includes("/") ? relDir.slice(0, relDir.lastIndexOf("/")) : "";
        const parentAbs = path.join(ROOT_DIR, parent);
        const parentEntries = listDir(parentAbs);
        return api.sendMessage(renderList(parent, parentEntries), event.threadID, (e, info) => {
            if (e || !info) return;
            global.GoatBot.onReply.set(info.messageID, {
                commandName: "file",
                messageID: info.messageID,
                author: event.senderID,
                relDir: parent,
                entries: parentEntries
            });
            deleteOldPage(api, Reply);
        });
    }

    const deleteMatch = body.match(/^delete\s+(.+)$/i);
    if (deleteMatch) {
        const nums = deleteMatch[1].trim().split(/\s+/).map(n => parseInt(n));
        let msg = "";
        for (const num of nums) {
            const target = entries[num - 1];
            if (!target) { msg += `❌ ${num} invalid\n`; continue; }
            const full = path.join(dirAbs, target.name);
            try {
                if (target.isDir) fs.rmdirSync(full, { recursive: true });
                else fs.unlinkSync(full);
                msg += `${target.isDir ? "📂" : "📄"} ${target.name} deleted\n`;
            } catch (err) {
                msg += `❌ Failed: ${target.name} (${err.message})\n`;
            }
        }
        return api.sendMessage(`⚡️Done:\n\n${msg}`, event.threadID, event.messageID);
    }

    const rawMatch = body.match(/^raw\s+(.+)$/i);
    if (rawMatch) {
        const nums = rawMatch[1].trim().split(/\s+/).map(n => parseInt(n));
        let msg = "";
        for (const num of nums) {
            const target = entries[num - 1];
            if (!target) { msg += `❌ ${num} invalid\n`; continue; }
            if (target.isDir) { msg += `❌ ${num}. ${target.name} is a folder, skipped\n`; continue; }
            if (target.isImage) { msg += `❌ ${num}. ${target.name} is an image, skipped\n`; continue; }
            const full = path.join(dirAbs, target.name);
            let code;
            try {
                code = fs.readFileSync(full, "utf8");
            } catch (e) {
                msg += `❌ ${num}. ${target.name} couldn't be read\n`;
                continue;
            }
            try {
                const res = await axios.post(`${PASTEBIN_API}/api/paste`, { code });
                if (res.data && res.data.url) {
                    msg += `📄 ${target.name}\n🔗 ${res.data.url}\n`;
                } else {
                    msg += `⚠️ ${target.name} - upload failed, no link received\n`;
                }
            } catch (err) {
                msg += `❌ ${target.name} - upload failed: ${err.message}\n`;
            }
        }
        return api.sendMessage(`⚡️Raw links:\n\n${msg}`, event.threadID, event.messageID);
    }

    const num = parseInt(body, 10);
    if (!Number.isInteger(num) || !entries[num - 1]) {
        return api.sendMessage("❌ Invalid reply. Send a number, \"back\", or \"delete 1,2,3\".", event.threadID, event.messageID);
    }
    const entry = entries[num - 1];
    const entryAbs = path.join(dirAbs, entry.name);
    const newRelDir = relDir ? `${relDir}/${entry.name}` : entry.name;

    if (entry.isDir) {
        const subEntries = listDir(entryAbs);
        return api.sendMessage(renderList(newRelDir, subEntries), event.threadID, (e, info) => {
            if (e || !info) return;
            global.GoatBot.onReply.set(info.messageID, {
                commandName: "file",
                messageID: info.messageID,
                author: event.senderID,
                relDir: newRelDir,
                entries: subEntries
            });
            deleteOldPage(api, Reply);
        });
    }

    if (entry.isImage) {
        return api.sendMessage(
            { body: `🖼️ ${entry.name}`, attachment: fs.createReadStream(entryAbs) },
            event.threadID,
            (e, info) => {
                if (e) return;
                deleteOldPage(api, Reply);
            },
            event.messageID
        );
    }

    return api.sendMessage(
        `📄 ${entry.name}\n✦Reply "raw" to get a paste link for this file\n✦Or use the delete command to remove it`,
        event.threadID,
        (e, info) => {
            if (e || !info) return;
            global.GoatBot.onReply.set(info.messageID, {
                commandName: "file",
                messageID: info.messageID,
                author: event.senderID,
                stage: "viewFile",
                filePath: newRelDir
            });
            deleteOldPage(api, Reply);
        },
        event.messageID
    );
  },

  onStart: async function ({ api, event, args }) {
    if (args[0] === "help") {
        return api.sendMessage(
`How to use:
•start <text> — filter files starting with text
•ext <text> — filter files by extension
•<text> — filter files containing text
•(blank) — show all files/folders
•Reply a number to open a folder or preview an image
•Reply "back" to go up a level
•Reply "delete 1,2,3" to delete
•Reply "raw 1,2,3" to get paste links for those files`,
            event.threadID, event.messageID);
    }

    let entries = listDir(path.join(ROOT_DIR, COMMANDS_RELDIR));
    let key = "⚡️All files/folders in commands:";

    if (args[0] === "start" && args[1]) {
        const word = args.slice(1).join(" ");
        entries = entries.filter(e => e.name.startsWith(word));
        key = `⚡️Files starting with: ${word}`;
    } else if (args[0] === "ext" && args[1]) {
        const ext = args[1];
        entries = entries.filter(e => e.name.endsWith(ext));
        key = `⚡️Files ending with: ${ext}`;
    } else if (args[0]) {
        const word = args.join(" ");
        entries = entries.filter(e => e.name.includes(word));
        key = `⚡️Files containing: ${word}`;
    }

    if (entries.length === 0) return api.sendMessage("⚡️No matching files/folders found.", event.threadID, event.messageID);

    return api.sendMessage(`${key}\n\n${renderList(COMMANDS_RELDIR, entries)}`, event.threadID, (e, info) => {
        if (e || !info) return;
        global.GoatBot.onReply.set(info.messageID, {
            commandName: "file",
            messageID: info.messageID,
            author: event.senderID,
            relDir: COMMANDS_RELDIR,
            entries
        });
    });
  }
};

View raw file