@wahaha216/koishi-plugin-steam-workshop
Version:
从 steam 创意工坊获取文件并上传,可选RPC推送至服务器下载
490 lines (482 loc) • 21.4 kB
JavaScript
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
});