UNPKG

n1cat-discord-script-manager

Version:

A Discord.js plugin for dynamic script management and execution

515 lines (451 loc) 17.8 kB
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, }); } }, };