UNPKG

@wahaha216/koishi-plugin-steam-workshop

Version:

从 steam 创意工坊获取文件并上传,可选RPC推送至服务器下载

490 lines (482 loc) 21.4 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/locales/zh-CN.yml var require_zh_CN = __commonJS({ "src/locales/zh-CN.yml"(exports2, module2) { module2.exports = { commands: { workshop: { description: "识别创意工坊物品链接", examples: "workshop https://steamcommunity.com/sharedfiles/filedetails/?id=xxxxxx", options: { download: "不询问直接下载", info: "仅返回详情信息(优先级更高)" }, messages: { title: "标题", releaseTime: "发布时间", updateTime: "更新时间", fileSize: "文件大小", game: "游戏", description: "描述", ask_download: "是否下载:{0}?({1}秒内回复:是|y|yes/否|n|no)", input_timeout: "输入超时", invalid_link: "不正确的链接格式", single_file: "{0}判断为单文件", multi_file: "{0}判断为多文件,合集或是有依赖文件", file_has_dep: "文件类型:{0},有依赖文件", file_collection: "文件类型:{0},合集'", download_info: "文件名称:{0},URL:{1}", download_retry: "文件{0}下载失败,正在重试 {1}/{2}", download_fail: "(部分)文件下载失败", request_fail: "获取创意工坊信息失败", request_retry: "获取创意工坊信息失败,正在重试 {0}/{1}", ask_push: "是否将下载地址推送至服务器?({0}秒内回复:是|y|yes/否|n|no)" } } }, _config: [{ $desc: "基础设置", autoRecognise: "自动识别创意工坊物品链接", askDownload: "询问是否下载文件" }, { requestRetries: "请求重试次数,0为不重试", downloadRetries: "下载重试次数,0为不重试", threadCount: "下载线程数", inputTimeout: "询问超时时间(毫秒)" }, { $desc: "RPC设置", rpc: "是否启用rpc推送" }, { rpcIp: "IP地址", rpcPort: "端口", rpcSecure: "加密连接", rpcSecret: "可选:授权令牌", rpcPolling: "轮询时间间隔,毫秒", rpcPollingCount: "轮询次数", rpcDir: "存储路径" }], rpc: { push: "已将 {0} 下载链接推送至服务器", complete: "{0} 下载完成", error: "{0} 下载失败,请自行检查远程服务器", timeout: "{0} 在指定时间({1})内未下载完成,不再等待,请自行检查远程服务器" } }; } }); // src/locales/en-US.yml var require_en_US = __commonJS({ "src/locales/en-US.yml"(exports2, module2) { module2.exports = { commands: { workshop: { description: "Recongnise Steam Workshop item link", examples: "workshop https://steamcommunity.com/sharedfiles/filedetails/?id=xxxxxx", options: { download: "Direct download", info: "Only infomation (first priority)" }, messages: { title: "Title", releaseTime: "Release Time", updateTime: "Update Time", fileSize: "FileSize", game: "Game", description: "Description", ask_download: "Download {title} ? (Reply in {timeout} seconds 是|y|yes / 否|n|no)", input_timeout: "Input timeout", invalid_link: "Invalid link", single_file: "{0} is single file", multi_file: "{0} is collections or has depend", file_has_depend: "File type: {0}, has denpen", file_collection: "File type: {0}, is collections", download_info: "Filename: {0}, url: {1}", download_retry: "File '{0}' download failed, retrying {1}/{2}", download_fail: "(Some) file download failed", request_fail: "Get steam workshop info failed", request_retry: "Get steam workshop info failed, retrying {1}/{2}", ask_push: "Is the download address pushed to the server? (Reply within {0} seconds: Yes |y|yes/No |n|no)" } } }, _config: [{ $desc: "Basic settings", autoRecognise: "Auto recongnise Steam Workshop item link", askDownload: "Ask download file" }, { requestRetries: "Request retries, 0 for no retries", downloadRetries: "Download retries, 0 for no retries", threadCount: "Download thread count", inputTimeout: "Ask download timeout, ms" }, { $desc: "RPC settings", rpc: "Whether to enable rpc push" }, { rpcIp: "IP address", rpcPort: "port", rpcSecure: "Encrypted connection", rpcSecret: "Optional: Authorization token", rpcPolling: "Polling time interval, milliseconds", rpcPollingCount: "Number of polling times", rpcDir: "Storage path" }], rpc: { push: "The {0} download link has been pushed to the server", complete: "{0} Download completed", error: "{0} Download failed, please check the remote server yourself", timeout: "{0} The download was not completed within the specified time ({1}). No more waiting. Please check the remote server yourself." } }; } }); // src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, http: () => http, inject: () => inject, logger: () => logger, name: () => name, retryCount: () => retryCount }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); // src/utils/sizeFormat.ts var sizeFormat = /* @__PURE__ */ __name((size) => { if (!size) return; if (typeof size === "string") { size = parseInt(size); } if (size < 1024) { return `${size} B`; } else if (size < 1024 ** 2) { return `${(size / 1024).toFixed(2)} KB`; } else if (size < 1024 ** 3) { return `${(size / 1024 ** 2).toFixed(2)} MB`; } else { return `${(size / 1024 ** 3).toFixed(2)} GB`; } }, "sizeFormat"); var timestampToDate = /* @__PURE__ */ __name((timestamp) => { const now = new Date(timestamp); const y = now.getFullYear(); const m = now.getMonth() + 1; const mm = m < 10 ? `0${m}` : m; const d = now.getDate(); const dd = d < 10 ? `0${d}` : d; return `${y}-${mm}-${dd} ${now.toTimeString().substring(0, 8)}`; }, "timestampToDate"); // src/error/overRetry.error.ts var OverRetryError = class extends Error { static { __name(this, "OverRetryError"); } constructor(message) { super(message); this.name = "OverRetryError"; } }; // src/utils/index.ts async function requestWithRetry(url, method, config = {}, retryIndex = 0) { try { const res = await http(method, url, config); return res.data; } catch (error) { if (retryIndex < retryCount) { logger.info( `${url} 请求失败,正在重试... ${retryIndex + 1}/${retryCount}` ); return await requestWithRetry(url, method, config, retryIndex + 1); } else { throw new OverRetryError(`请求失败,超过最大重试次数: ${url}`); } } } __name(requestWithRetry, "requestWithRetry"); // src/index.ts var name = "steam-workshop"; var WORKSHOP_API = "https://steamworkshopdownloader.io/api/details/file"; var Config = import_koishi.Schema.intersect([ import_koishi.Schema.object({ autoRecognise: import_koishi.Schema.boolean().default(true), askDownload: import_koishi.Schema.boolean().default(true) }), import_koishi.Schema.union([ import_koishi.Schema.object({ askDownload: import_koishi.Schema.const(true), requestRetries: import_koishi.Schema.number().default(5).min(0).max(10), downloadRetries: import_koishi.Schema.number().default(5).min(0).max(10), threadCount: import_koishi.Schema.number().default(4).min(1).max(16), inputTimeout: import_koishi.Schema.number().default(6e4).min(5e3) }), import_koishi.Schema.object({}) ]), import_koishi.Schema.object({ rpc: import_koishi.Schema.boolean().default(false) }), import_koishi.Schema.union([ import_koishi.Schema.object({ rpc: import_koishi.Schema.const(true).required(), rpcIp: import_koishi.Schema.string().required(), rpcPort: import_koishi.Schema.number().default(6800).min(1).max(65535), rpcSecure: import_koishi.Schema.boolean().default(false), rpcSecret: import_koishi.Schema.string(), rpcPolling: import_koishi.Schema.number().default(1e4).min(1e3), rpcPollingCount: import_koishi.Schema.number().default(60).min(1), rpcDir: import_koishi.Schema.string().required() }), import_koishi.Schema.object({}) ]) ]).i18n({ "zh-CN": require_zh_CN()._config, "en-US": require_en_US()._config }); var inject = { required: ["http", "logger"] }; var http; var logger; var retryCount; function apply(ctx, config) { ctx.i18n.define("en-US", require_en_US()); ctx.i18n.define("zh-CN", require_zh_CN()); const regexp = /^https:\/\/steamcommunity.com\/(sharedfiles|workshop)\/filedetails\/\?id=\d+/; logger = ctx.logger("wahaha216-steam-workshop"); http = ctx.http; retryCount = config.requestRetries; ctx.command("workshop <url:string>").option("download", "-d").option("info", "-i").option("name", "-n [name:string]").option("push", "-p").action(async ({ session, options }, url) => { const id = session.messageId; const timeout = config.inputTimeout / 1e3; const formatFileName = /* @__PURE__ */ __name((item) => { if (options.name) return options.name; const invalidReg = /[\\/:\*\?"\<\>\|\r\n]/g; const ext = item.filename.substring(item.filename.lastIndexOf(".")); const name2 = item.title.replace(invalidReg, " "); const download_name = `${name2.trim()}${ext}`; logger.info( session.text(".download_info", [download_name, item.file_url]) ); return download_name; }, "formatFileName"); const rpcRequest = /* @__PURE__ */ __name(async (method, params) => { const protocol = config.rpcSecure ? "https://" : "http://"; const url2 = `${protocol}${config.rpcIp}:${config.rpcPort}/jsonrpc`; return await requestWithRetry(url2, "POST", { data: { //消息id,aria2会原样返回这个id,可以自动生成也可以用其他唯一标识 id: (/* @__PURE__ */ new Date()).getTime().toString(), //固定值 jsonrpc: "2.0", //方法名,具体参考上方“方法列表”链接,本例中为“添加下载任务” method, //params为数组 params } }); }, "rpcRequest"); const rpcServer = /* @__PURE__ */ __name(async (url2, fileName) => { const token = `token:${config.rpcSecret}`; const addTaskParams = [ token, [url2], { dir: config.rpcDir, out: fileName } ]; if (!config.rpcSecret) addTaskParams.shift(); const addTaskRes = await rpcRequest("aria2.addUri", addTaskParams); await session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text("rpc.push", [fileName])) ]); const key = addTaskRes.result; let intervalCount = 0; do { await ctx.sleep(config.rpcPolling); try { const statusParams = [token, key]; if (!config.rpcSecret) statusParams.shift(); const statusRes = await rpcRequest( "aria2.tellStatus", statusParams ); switch (statusRes.result.status) { case "complete": return await session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text("rpc.complete", [fileName])) ]); case "error": return await session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text("rpc.error", [fileName])) ]); default: break; } } catch (error) { } intervalCount++; } while (intervalCount < config.rpcPollingCount); return await session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text("rpc.timeout", [fileName])) ]); }, "rpcServer"); if (regexp.test(url)) { const u = new URL(url); const workId = u.searchParams.get("id"); let res; try { res = await requestWithRetry(WORKSHOP_API, "POST", { data: `[${workId}]`, responseType: "json" }); } catch (error) { if (error instanceof OverRetryError) { session.send([import_koishi.h.quote(id), import_koishi.h.text(session.text(".request_fail"))]); return; } } const data = res[0]; if (data.num_children === 0) { logger.info(session.text(".single_file", [data.title])); const fragment = [ import_koishi.h.quote(id), import_koishi.h.text(`${session.text(".title")}: ${data.title} `), import_koishi.h.text( `${session.text(".releaseTime")}: ${timestampToDate( data.time_created * 1e3 )} ` ), import_koishi.h.text( `${session.text(".updateTime")}: ${timestampToDate( data.time_updated * 1e3 )} ` ), import_koishi.h.text( `${session.text(".fileSize")}: ${sizeFormat(data.file_size)} ` ), import_koishi.h.text(`${session.text(".game")}: ${data.app_name} `), import_koishi.h.text( `${session.text(".description")}: ${data.file_description} ` ), import_koishi.h.image(data.preview_url) ]; if (config.askDownload && !options.info && !options.download) { fragment.push( import_koishi.h.text("=".repeat(20) + "\n"), import_koishi.h.text(session.text(".ask_download", [data.title, timeout])) ); } await session.send(fragment); if (options.info) return; if (config.askDownload) { if (!options.download) { const download = await session.prompt(config.inputTimeout); if (!download) return [import_koishi.h.quote(id), import_koishi.h.text(session.text(".input_timeout"))]; if (!["是", "y", "yes"].includes(download.toLocaleLowerCase())) return; } let push = options.push || false; if (config.rpc && !options.push) { await session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text(".ask_push"), [timeout]) ]); const ans = await session.prompt(config.inputTimeout); push = ["是", "y", "yes"].includes(ans.toLocaleLowerCase()); } const title = formatFileName(data); const retries = config.downloadRetries; let success = true; for (let i = 0; i <= retries; i++) { const result = await session.send([ import_koishi.h.file(data.file_url, { title }) ]); if (config.rpc && push) rpcServer(data.file_url, title); if (result.length) { return; } else if (i === retries - 1) { success = false; } else { logger.info( session.text(".download_retry", [title, i + 1, retries]) ); } } if (!success) { session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text(".download_fail")) ]); } } } else { logger.info(session.text(".multi_file", [data.title])); let workIds = data.children.map((item) => item.publishedfileid); const multiRes = []; try { if (workIds.length <= 50) { const res2 = await requestWithRetry( WORKSHOP_API, "POST", { data: `[${workIds.join(",")}]`, responseType: "json" } ); multiRes.push(...res2); } else { do { const ids = workIds.slice(0, 50).join(","); const res2 = await requestWithRetry( WORKSHOP_API, "POST", { data: `[${ids}]`, responseType: "json" } ); multiRes.push(...res2); workIds.splice(0, 50); } while (workIds.length <= 50); } } catch (error) { if (error instanceof OverRetryError) { session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text(".request_fail")) ]); return; } } if (multiRes) { const result = (0, import_koishi.segment)("figure"); const buildContent = /* @__PURE__ */ __name((item) => { const fragment = [ import_koishi.h.text(`${session.text(".title")}: ${item.title} `), import_koishi.h.text( `${session.text(".fileSize")}: ${sizeFormat( item.file_size )} ` ), import_koishi.h.text(`${session.text(".game")}: ${item.app_name} `), import_koishi.h.text( `${session.text(".description")}: ${item.file_description} ` ), import_koishi.h.image(item.preview_url) ]; result.children.push( (0, import_koishi.segment)( "message", { userId: session.event.selfId, nickname: "workshop info" }, fragment ) ); }, "buildContent"); if (data.file_type === 0) { logger.info(session.text(".file_has_depend", [data.file_type])); buildContent(data); } else { logger.info(session.text(".file_collection", [data.file_type])); } multiRes.forEach(buildContent); await session.send(result); if (config.askDownload && !options.info && !options.download) { await (0, import_koishi.sleep)(2e3); await session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text(".ask_download", [data.title, timeout])) ]); } if (options.info) return; if (config.askDownload) { if (!options.download) { const download = await session.prompt(config.inputTimeout); if (!download) return [import_koishi.h.quote(id), import_koishi.h.text(session.text(".input_timeout"))]; if (!["是", "y", "yes"].includes(download.toLocaleLowerCase())) return; } let push = options.push || false; if (config.rpc && !options.push) { await session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text(".ask_push"), [timeout]) ]); const ans = await session.prompt(config.inputTimeout); push = ["是", "y", "yes"].includes(ans.toLocaleLowerCase()); } let success = true; if (data.file_type === 0) { multiRes.unshift(data); } let failIds = multiRes.map((item) => item.publishedfileid); const retries = config.downloadRetries; for (let i = 0; i <= retries; i++) { const list = multiRes.filter( (item) => failIds.some((id2) => id2 === item.publishedfileid) ); for (const item of list) { const title = formatFileName(item); const result2 = await session.send([ import_koishi.h.file(item.file_url, { title }) ]); if (config.rpc && push) rpcServer(item.file_url, title); if (result2.length) { const index = failIds.indexOf(item.publishedfileid); if (index !== -1) { failIds.splice(index, 1); } } else if (i === retries - 1) { success = false; } } if (failIds.length) { logger.info( session.text(".download_retry", ["", i + 1, retries]) ); } } if (!success) { session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text(".download_fail")) ]); } } } } } else { session.send([import_koishi.h.quote(id), import_koishi.h.text(session.text(".invalid_link"))]); } }); ctx.on("message", (session) => { if (config.autoRecognise) { const text = session.content; if (regexp.test(text)) { session.execute(`workshop ${text}`); } } }); } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, http, inject, logger, name, retryCount });