UNPKG

@wahaha216/koishi-plugin-steam-workshop

Version:

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

589 lines (579 loc) 23.9 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_depend: "文件类型:{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: "已将该链接下的文件的下载链接推送至服务器", complete: "该链接下的文件下载完成", error: "该链接下的文件(部分)下载失败\n{0}", timeout: "该链接下的文件在指定时间内未下载完成,不再检查下载状态,请自行检查远程服务器" } }; } }); // 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 download link has been pushed to the server", complete: "Download completed", error: "Download failed, reson:\n{0}", timeout: "The download was not completed within the specified time. No more checking status. Please check the remote server yourself." } }; } }); // src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, inject: () => inject, name: () => name }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); // 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(http, logger, retryCount, 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( http, logger, retryCount, url, method, config, retryIndex + 1 ); } else { throw new OverRetryError(`请求失败,超过最大重试次数: ${url}`); } } } __name(requestWithRetry, "requestWithRetry"); function formatFileName(logger, item, session) { 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; } __name(formatFileName, "formatFileName"); // src/utils/const.ts var WORKSHOP_API = "https://steamworkshopdownloader.io/api/details/file"; // 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/entity/SteamWorkshop.ts var SteamWorkshop = class { static { __name(this, "SteamWorkshop"); } session; http; logger; config; title; workshopInfo; /** * 单文件 */ singleFile; fileInfos; constructor(session, http, logger, config) { this.session = session; this.http = http; this.logger = logger; this.config = config; } /** * 解析steam创意工坊链接 * @param url steam创意工坊链接 */ async analyzeUrl(url) { const u = new URL(url); const workshopId = u.searchParams.get("id"); this.workshopInfo = await requestWithRetry( this.http, this.logger, this.config.requestRetries, WORKSHOP_API, "POST", { data: `[${workshopId}]`, responseType: "json" } ); const firstInfo = this.workshopInfo[0]; this.singleFile = firstInfo.num_children === 0; this.title = firstInfo.title; if (!this.singleFile) { const workIds = firstInfo.children.map((item) => item.publishedfileid); if (workIds.length <= 50) { const res = await requestWithRetry( this.http, this.logger, this.config.requestRetries, WORKSHOP_API, "POST", { data: `[${workIds.join(",")}]`, responseType: "json" } ); this.workshopInfo.push(...res); } else { do { const ids = workIds.slice(0, 50).join(","); const res = await requestWithRetry( this.http, this.logger, this.config.requestRetries, WORKSHOP_API, "POST", { data: `[${ids}]`, responseType: "json" } ); this.workshopInfo.push(...res); workIds.splice(0, 50); } while (workIds.length); } } const infos = []; this.workshopInfo.forEach((item) => { infos.push({ title: item.title, releaseTimestamp: item.time_created, releaseTime: timestampToDate(item.time_created * 1e3), updateTimestamp: item.time_updated, updateTime: timestampToDate(item.time_updated * 1e3), fileSize: item.file_size, formatFileSize: sizeFormat(item.file_size), game: item.app_name, description: item.file_description, imageUrl: item.preview_url, fileUrl: item.file_url, fileName: formatFileName(this.logger, item, this.session) }); }); this.fileInfos = infos; } /** * 生成批量下载的请求体 * @returns 批量下载的请求体 */ buildRpcDownloadBody() { const multiParams = this.fileInfos.map((item) => { const params = [ `token:${this.config.rpcSecret}`, [item.fileUrl], { dir: this.config.rpcDir, out: item.fileName } ]; if (!this.config.rpcSecret) params.shift(); return { methodName: "aria2.addUri", params }; }); return { id: (/* @__PURE__ */ new Date()).getTime().toString(), jsonrpc: "2.0", method: "system.multicall", params: [multiParams] }; } /** * 生成获取下载状态的请求体 * @param guids aria2返回的下载任务guid列表 * @returns 获取下载状态的请求体 */ buildRpcStatusBody(guids) { return { id: (/* @__PURE__ */ new Date()).getTime().toString(), jsonrpc: "2.0", method: "system.multicall", params: [ guids.map((guid) => { const params = [`token:${this.config.rpcSecret}`, guid]; if (!this.config.rpcSecret) params.shift(); return { methodName: "aria2.tellStatus", params }; }) ] }; } /** * 获取是否为单文件 * @returns 是否为单文件 */ getSingleFile() { return this.singleFile; } getWorkshopInfo() { return this.workshopInfo; } getFileInfos() { return this.fileInfos; } getTitle() { return this.title; } }; // src/index.ts var name = "steam-workshop"; 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"] }; 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+/; const logger = ctx.logger("wahaha216-steam-workshop"); 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 rpcRequest = /* @__PURE__ */ __name(async (body) => { const protocol = config.rpcSecure ? "https://" : "http://"; const url2 = `${protocol}${config.rpcIp}:${config.rpcPort}/jsonrpc`; return await requestWithRetry( ctx.http, logger, config.requestRetries, url2, "POST", { data: body } ); }, "rpcRequest"); const rpcServer = /* @__PURE__ */ __name(async (steamWorkshop) => { const rpcDownloadBody = steamWorkshop.buildRpcDownloadBody(); logger.info(rpcDownloadBody); const addTaskRes = await rpcRequest(rpcDownloadBody); await session.send([import_koishi.h.quote(id), import_koishi.h.text(session.text("rpc.push"))]); const result = addTaskRes.result; const keys = result.map((r) => r[0]); let intervalCount = 0; do { await ctx.sleep(config.rpcPolling); try { const rpcStatusBody = steamWorkshop.buildRpcStatusBody(keys); const statusRes = await rpcRequest(rpcStatusBody); const status = statusRes.result.map((item) => item[0].status); const success = status.every((item) => item === "complete"); if (success) { return await session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text("rpc.complete")) ]); } const someError = status.some((item) => item === "error"); if (someError) { const errorMsg = statusRes.result.map( (item) => item[0].errorMessage ); return await session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text("rpc.error", [errorMsg[0]])) ]); } } catch (error) { } intervalCount++; } while (intervalCount < config.rpcPollingCount); return await session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text("rpc.timeout")) ]); }, "rpcServer"); if (regexp.test(url)) { const steamWorkshop = new SteamWorkshop( session, ctx.http, logger, config ); try { await steamWorkshop.analyzeUrl(url); } catch (error) { if (error instanceof OverRetryError) { session.send([import_koishi.h.quote(id), import_koishi.h.text(session.text(".request_fail"))]); return; } } const singleFile = steamWorkshop.getSingleFile(); if (singleFile) { const fileInfos = steamWorkshop.getFileInfos(); const fileinfo = fileInfos[0]; const title = steamWorkshop.getTitle(); logger.info(session.text(".single_file", [title])); const fragment = [ import_koishi.h.quote(id), import_koishi.h.text(`${session.text(".title")}: ${fileinfo.title} `), import_koishi.h.text( `${session.text(".releaseTime")}: ${fileinfo.releaseTime} ` ), import_koishi.h.text(`${session.text(".updateTime")}: ${fileinfo.updateTime} `), import_koishi.h.text( `${session.text(".fileSize")}: ${fileinfo.formatFileSize} ` ), import_koishi.h.text(`${session.text(".game")}: ${fileinfo.game} `), import_koishi.h.text( `${session.text(".description")}: ${fileinfo.description} ` ), import_koishi.h.image(fileinfo.imageUrl) ]; if (config.askDownload && !options.info && !options.download) { fragment.push( import_koishi.h.text("=".repeat(20) + "\n"), import_koishi.h.text(session.text(".ask_download", [fileinfo.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 retries = config.downloadRetries; let success = true; for (let i = 0; i <= retries; i++) { const result = await session.send([ import_koishi.h.file(fileinfo.fileUrl, { title: fileinfo.fileName }) ]); if (result.length) { break; } else if (i === retries - 1) { success = false; } else { logger.info( session.text(".download_retry", [ fileinfo.fileName, i + 1, retries ]) ); } } if (!success) { session.send([ import_koishi.h.quote(id), import_koishi.h.text(session.text(".download_fail")) ]); } if (config.rpc && push) rpcServer(steamWorkshop); } } else { const title = steamWorkshop.getTitle(); logger.info(session.text(".multi_file", [title])); const workshopInfo = steamWorkshop.getWorkshopInfo(); const workshopFileinfo = steamWorkshop.getFileInfos(); const fileType = workshopInfo[0].file_type; if (fileType === 0) { logger.info(session.text(".file_has_depend", [fileType])); } else { logger.info(session.text(".file_collection", [fileType])); workshopInfo.shift(); workshopFileinfo.shift(); } const result = (0, import_koishi.segment)("figure"); workshopFileinfo.forEach((item) => { const fragment = [ import_koishi.h.text(`${session.text(".title")}: ${item.title} `), import_koishi.h.text(`${session.text(".fileSize")}: ${item.fileSize} `), import_koishi.h.text(`${session.text(".releaseTime")}: ${item.releaseTime} `), import_koishi.h.text(`${session.text(".updateTime")}: ${item.updateTime} `), import_koishi.h.text(`${session.text(".game")}: ${item.game} `), import_koishi.h.text( `${session.text(".description")}: ${item.description} ` ), import_koishi.h.image(item.imageUrl) ]; result.children.push( (0, import_koishi.segment)( "message", { userId: session.event.selfId, nickname: "workshop info" }, fragment ) ); }); 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", [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; let failIds = workshopInfo.map((item) => item.publishedfileid); const retries = config.downloadRetries; for (let i = 0; i <= retries; i++) { const list = workshopInfo.filter( (item) => failIds.some((id2) => id2 === item.publishedfileid) ); for (const item of list) { const title2 = formatFileName(logger, item, session); const result2 = await session.send([ import_koishi.h.file(item.file_url, { title: title2 }) ]); 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")) ]); } if (config.rpc && push) rpcServer(steamWorkshop); } } } 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, inject, name });