n1cat-discord-script-manager
Version:
A Discord.js plugin for dynamic script management and execution
515 lines (451 loc) • 17.8 kB
JavaScript
const fs = require("fs");
const moment = require("moment");
const path = require("path");
const {
SlashCommandBuilder,
ActionRowBuilder,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
} = require("discord.js");
const { loadModule } = require("../utils/moduleLoader");
const Logger = require("../utils/logger");
// 用於追蹤函式調用的輔助函數
function getCallerInfo() {
const stack = new Error().stack;
const callerLine = stack.split("\n")[3]; // 跳過 Error 和 getCallerInfo 的堆疊
const match = callerLine.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
if (match) {
const [, functionName, file, line, column] = match;
return {
function: functionName,
file: file.split("/").pop(), // 只取檔案名稱
line,
column,
};
}
return {
function: "unknown",
file: "unknown",
line: "unknown",
column: "unknown",
};
}
// 日誌輸出函數
function log(message, isError = false) {
const caller = getCallerInfo();
const debugMode = this.options?.debug ?? false;
// 如果是錯誤或 debug 模式開啟,則輸出詳細日誌
if (isError || debugMode) {
console.log(`[${caller.file}:${caller.line}] ${message}`);
}
}
// 清除模組快取
function clearModuleCache(modulePath) {
Object.keys(require.cache).forEach((key) => {
if (key.includes(modulePath)) {
delete require.cache[key];
}
});
}
module.exports = {
data: new SlashCommandBuilder()
.setName("interaction")
.setDescription("處理按鈕互動"),
async execute(interaction) {
const logger = Logger.createContextLogger({
module: "InteractionCommand",
debug: interaction.client.config?.debug || false,
});
try {
const scriptManager = interaction.client.scriptManager;
if (!scriptManager) {
logger.error("找不到 ScriptManager 實例");
return await interaction.reply({
content: "❌ 系統錯誤:找不到腳本管理器",
ephemeral: true,
});
}
const customId = interaction.customId;
const user = interaction.user;
// 處理腳本上傳請求
if (customId === "approve") {
logger.log("處理批准請求");
// 獲取原始消息的內容
const originalMessage = interaction.message;
if (!originalMessage) {
throw new Error("找不到原始消息");
}
// 安全地獲取上傳的文件名
let uploadedFileName = "未知檔案";
let attachmentUrl = null;
try {
// 嘗試從原始互動中獲取附件
if (
originalMessage.interaction &&
originalMessage.interaction.options
) {
const attachment =
originalMessage.interaction.options.getAttachment("script");
if (attachment) {
uploadedFileName = attachment.name;
attachmentUrl = attachment.url;
}
}
// 如果上面的方法失敗,嘗試從消息附件中獲取
if (uploadedFileName === "未知檔案") {
const messageAttachment = originalMessage.attachments.first();
if (messageAttachment) {
uploadedFileName = messageAttachment.name;
attachmentUrl = messageAttachment.url;
}
}
logger.log(`原始上傳文件名: ${uploadedFileName}`);
} catch (error) {
logger.error(`獲取附件時出錯: ${error.message}`);
}
// 從 embed 標題中提取腳本名稱
const embed = originalMessage.embeds[0];
if (!embed) {
logger.error("找不到 embed");
throw new Error("找不到 embed");
}
const scriptNameMatch = embed.title.match(/已上傳 `(\d{13}\.js)`/);
if (!scriptNameMatch) {
logger.error(`無法從 embed 標題中提取腳本名稱,標題: ${embed.title}`);
throw new Error("無法從 embed 標題中提取腳本名稱");
}
const scriptName = scriptNameMatch[1];
logger.log(`提取到腳本名稱: ${scriptName}`);
// 創建模態框用於輸入腳本描述
const modal = new ModalBuilder()
.setCustomId(`script_description_${scriptName}`)
.setTitle(`為腳本 ${uploadedFileName} 添加描述`);
const descriptionInput = new TextInputBuilder()
.setCustomId("script_description")
.setLabel("腳本描述")
.setStyle(TextInputStyle.Paragraph)
.setPlaceholder("請輸入腳本的簡要說明(可選)")
.setRequired(false)
.setMaxLength(1000);
const actionRow = new ActionRowBuilder().addComponents(
descriptionInput
);
modal.addComponents(actionRow);
// 顯示模態框
await interaction.showModal(modal);
return;
}
// 處理腳本描述提交
else if (
interaction.isModalSubmit() &&
interaction.customId.startsWith("script_description_")
) {
logger.log(`處理 Modal Submit: ${interaction.customId}`);
// 立刻延遲更新,避免超時
await interaction.deferUpdate();
logger.log("已成功延遲更新 Modal Submit");
const scriptName = interaction.customId.replace(
"script_description_",
""
);
const description =
interaction.fields.getTextInputValue("script_description");
logger.log(`收到腳本描述: ${description}`);
// 繼續原有的批准邏輯,但加入描述
try {
// 從頻道歷史紀錄中獲取原始消息
const originalMessage = await interaction.channel.messages.fetch(
interaction.message.id
);
if (!originalMessage) {
throw new Error("找不到原始消息");
}
// 安全地獲取上傳的文件名
let uploadedFileName = "未知檔案";
let attachmentUrl = null;
try {
// 嘗試從原始互動中獲取附件
if (
originalMessage.interaction &&
originalMessage.interaction.options
) {
const attachment =
originalMessage.interaction.options.getAttachment("script");
if (attachment) {
uploadedFileName = attachment.name;
attachmentUrl = attachment.url;
}
}
// 如果上面的方法失敗,嘗試從消息附件中獲取
if (uploadedFileName === "未知檔案") {
const messageAttachment = originalMessage.attachments.first();
if (messageAttachment) {
uploadedFileName = messageAttachment.name;
attachmentUrl = messageAttachment.url;
}
}
logger.log(`原始上傳文件名: ${uploadedFileName}`);
} catch (error) {
logger.error(`獲取附件時出錯: ${error.message}`);
}
// 從 embed 標題中提取腳本名稱
const embed = originalMessage.embeds[0];
if (!embed) {
logger.error("找不到 embed");
throw new Error("找不到 embed");
}
const scriptNameMatch = embed.title.match(/已上傳 `(\d{13}\.js)`/);
if (!scriptNameMatch) {
logger.error(
`無法從 embed 標題中提取腳本名稱,標題: ${embed.title}`
);
throw new Error("無法從 embed 標題中提取腳本名稱");
}
scriptName = scriptNameMatch[1];
logger.log(`提取到腳本名稱: ${scriptName}`);
// 檢查是否為 bot owner
const isOwner = scriptManager.options.allowedUsers.includes(user.id);
if (!isOwner) {
logger.error("非 bot owner 嘗試使用命令");
return await interaction.reply({
content: "❌ 只有 bot owner 才能批准此腳本",
ephemeral: true,
});
}
// 獲取上傳者的 ID
const uploaderId = originalMessage.interaction.user.id;
logger.log(`上傳者 ID: ${uploaderId}`);
// 讀取臨時腳本文件
const tempScriptPath = path.join(
scriptManager.options.scriptFolder,
uploaderId,
`temp_${scriptName}`
);
logger.log(`臨時腳本路徑: ${tempScriptPath}`);
if (!fs.existsSync(tempScriptPath)) {
logger.error(`找不到臨時腳本文件: ${tempScriptPath}`);
throw new Error("找不到臨時腳本文件");
}
const scriptContent = fs.readFileSync(tempScriptPath, "utf8");
// 確保腳本目錄存在
const scriptFolder = path.resolve(scriptManager.options.scriptFolder);
if (!fs.existsSync(scriptFolder)) {
logger.log(`創建腳本目錄: ${scriptFolder}`);
fs.mkdirSync(scriptFolder, { recursive: true });
}
// 獲取上傳者信息
const uploader = originalMessage.interaction.user;
const metadata = {
author: uploader.username,
createdAt: moment().utc().format("YYYY/MM/DD HH:mm:ss"),
uploaderId: uploaderId,
attachmentUrl: attachmentUrl, // 使用安全獲取的 URL
originalFileName: uploadedFileName, // 使用安全獲取的文件名
};
// 修改頭部注釋,加入描述
const headerComment = `/*
此檔案為使用者腳本
原始腳本名稱: ${metadata.originalFileName}
腳本檔案名稱: ${scriptName}
由 ${metadata.author} 在 ${metadata.createdAt} (UTC) 上傳
原始附件鏈接: ${metadata.attachmentUrl || "N/A"}
腳本描述: ${description || "暫無描述"}
*/
`;
const finalScriptContent = headerComment + scriptContent;
// 保存腳本到主腳本目錄
const scriptPath = path.join(scriptFolder, scriptName);
logger.log(`保存腳本: ${scriptPath}`);
fs.writeFileSync(scriptPath, finalScriptContent);
// 刪除臨時文件
fs.unlinkSync(tempScriptPath);
// 清除腳本快取
clearModuleCache(scriptPath);
logger.log(`已清除腳本快取: ${scriptPath}`);
// 重新載入腳本
logger.log("開始重新載入腳本");
try {
// 清除快取
delete require.cache[require.resolve(scriptPath)];
// 載入腳本
const script = require(scriptPath);
// 檢查腳本是否為函數
if (typeof script === "function") {
// 更新元數據,加入描述
metadata.description = description || "暫無描述";
scriptManager.scripts.set(scriptName, {
name: scriptName,
path: scriptPath,
module: script,
enabled: true,
metadata: metadata,
});
logger.log(`✅ 成功載入腳本: ${scriptName}`);
} else {
throw new Error("Script must export a function");
}
} catch (error) {
logger.error(`載入腳本失敗: ${error.message}`, { showStack: true });
throw error;
}
// 更新消息
logger.log("更新消息");
await interaction.editReply({
content: `✅ 腳本 \`${scriptName}\` 已成功上傳並啟用\n原始檔案: \`${uploadedFileName}\`\n描述: ${
description || "暫無描述"
}\n\n${attachmentUrl || ""}`,
components: [],
});
logger.log("消息更新完成");
} catch (error) {
logger.error(`腳本保存失敗: ${error.message}`, { showStack: true });
await interaction.reply({
content: `❌ 保存腳本時出錯:${error.message}`,
ephemeral: true,
});
}
}
// 處理腳本拒絕請求
else if (customId === "reject") {
logger.log("處理拒絕請求");
try {
// 獲取原始消息的內容
const originalMessage = interaction.message;
if (!originalMessage) {
throw new Error("找不到原始消息");
}
// 從 embed 標題中提取腳本名稱
const embed = originalMessage.embeds[0];
if (!embed) {
logger.error("找不到 embed");
throw new Error("找不到 embed");
}
const scriptNameMatch = embed.title.match(/已上傳 `(\d{13}\.js)`/);
if (!scriptNameMatch) {
// 尝试新的匹配模式
const newScriptNameMatch = embed.title.match(
/已上傳 `(\d{13}\.js)` \(原始檔案: `(.+?)`\)/
);
if (!newScriptNameMatch) {
logger.error(
`無法從 embed 標題中提取腳本名稱,標題: ${embed.title}`
);
throw new Error("無法從 embed 標題中提取腳本名稱");
}
const scriptName = newScriptNameMatch[1];
const originalFileName = newScriptNameMatch[2];
logger.log(
`提取到腳本名稱: ${scriptName}, 原始檔案名稱: ${originalFileName}`
);
}
// 檢查是否為 bot owner
const isOwner = scriptManager.options.allowedUsers.includes(user.id);
if (!isOwner) {
return await interaction.reply({
content: "❌ 只有 bot owner 才能拒絕此腳本",
ephemeral: true,
});
}
// 獲取上傳者的 ID
const uploaderId = originalMessage.interaction.user.id;
logger.log(`上傳者 ID: ${uploaderId}`);
// 獲取附件鏈接
const attachmentUrl =
originalMessage.attachments.first()?.url || null;
logger.log(`附件鏈接: ${attachmentUrl}`);
// 刪除臨時文件
const tempScriptPath = path.join(
scriptManager.options.scriptFolder,
uploaderId,
`temp_${scriptName}`
);
logger.log(`臨時腳本路徑: ${tempScriptPath}`);
if (fs.existsSync(tempScriptPath)) {
fs.unlinkSync(tempScriptPath);
}
await interaction.deferUpdate();
await interaction.message.edit({
content: `❌ 腳本 \`${scriptName}\` 已被拒絕\n\n${
attachmentUrl || ""
}`,
components: [],
files: [], // 清除附件
});
} catch (error) {
logger.error(`互動處理失敗: ${error.message}`, { showStack: true });
await interaction.reply({
content: `❌ 處理請求時出錯:${error.message}`,
ephemeral: true,
});
}
}
// 處理刪除腳本的確認和取消
else if (customId.startsWith("confirm_delete_")) {
logger.log("處理刪除腳本確認");
try {
const scriptName = customId.replace("confirm_delete_", "");
const scriptManager = interaction.client.scriptManager;
if (!scriptManager) {
logger.error("找不到腳本管理器");
return await interaction.reply({
content: "❌ 找不到腳本管理器",
ephemeral: true,
});
}
const script = scriptManager.scripts.get(scriptName);
if (!script) {
logger.error(`找不到腳本: ${scriptName}`);
return await interaction.reply({
content: `❌ 找不到腳本:${scriptName}`,
ephemeral: true,
});
}
// 檢查是否為已停用腳本
if (script.enabled !== false) {
logger.error(`嘗試刪除未停用的腳本: ${scriptName}`);
return await interaction.reply({
content: `❌ 只能刪除已停用的腳本。請先使用 /managescript 停用該腳本。`,
ephemeral: true,
});
}
// 刪除腳本文件
const scriptPath = script.path;
if (fs.existsSync(scriptPath)) {
fs.unlinkSync(scriptPath);
logger.log(`已刪除腳本文件: ${scriptPath}`);
}
// 從腳本管理器中移除腳本
scriptManager.scripts.delete(scriptName);
logger.log(`已從腳本管理器中移除腳本: ${scriptName}`);
// 清除模組快取
delete require.cache[require.resolve(scriptPath)];
await interaction.update({
content: `✅ 腳本 \`${scriptName}\` 已成功刪除。`,
components: [],
});
} catch (error) {
logger.error(`刪除腳本時出錯: ${error.message}`, { showStack: true });
await interaction.reply({
content: `❌ 刪除腳本時出錯:${error.message}`,
ephemeral: true,
});
}
}
// 處理取消刪除
else if (customId === "cancel_delete") {
logger.log("取消刪除腳本");
await interaction.update({
content: "❌ 已取消刪除腳本。",
components: [],
});
}
} catch (error) {
logger.error(`互動處理失敗: ${error.message}`, { showStack: true });
await interaction.reply({
content: `❌ 處理請求時出錯:${error.message}`,
ephemeral: true,
});
}
},
};