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