UNPKG

koishi-plugin-booru-pixiv

Version:
367 lines (361 loc) 16.8 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; 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 name in all) __defProp(target, name, { get: all[name], 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); var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; // packages/pixiv/src/locales/zh-CN.schema.yml var require_zh_CN_schema = __commonJS({ "packages/pixiv/src/locales/zh-CN.schema.yml"(exports, module2) { module2.exports = { $description: "搜索设置", $inner: [{ endpoint: "Pixiv 的 API Root", token: "Pixiv 的 Refresh Token", target: { $description: "搜索目标类型", $inner: ["标签部分一致", "标签完全一致", "标题或介绍说明"] }, sort: { $description: "搜索结果排序方式", $inner: ["上传时间较近优先", "上传时间较远优先", "收藏数较多优先 (需要 Pixiv Premium)"] }, duration: { $description: "搜索时间范围", $inner: ["24 小时内", "一周内", "一个月内", "全部时间", "自定义"] }, minBookmarks: "最少收藏数,仅在设置了 Token 并有 Pixiv Premium 的情况下可用", maxBookmarks: "最大收藏数,仅在设置了 Token 并有 Pixiv Premium 的情况下可用", rank: { $description: "年龄分级", $inner: ["全年龄", "R18", "R18G"] }, ai: { $description: "是否允许搜索AI作品", $inner: ["不允许AI作品", "允许AI作品"] } }, { bypassMethod: { $description: "突破 Pixiv 站点图片防外部引用检测的方式。[参考](https://booru.koishi.chat/zh-CN/plugins/pixiv.html#bypass-pixiv-detection)", $inner: ["使用现有反代服务", "使用插件本地反代", "下载到 assets 缓存"] }, proxy: { $description: "Pixiv 反代服务。", $inner: ["i.pixiv.re", "i.pixiv.cat", "i.pixiv.nl", { $description: "自定义", endpoint: "反代服务的地址。" }] }, route: "反代服务的路径(需在 server 插件配置中填写 `selfUrl`)。", aesKey: "AES 加密密钥" }] }; } }); // packages/pixiv/src/index.ts var src_exports = {}; __export(src_exports, { default: () => src_default }); module.exports = __toCommonJS(src_exports); var import_node_crypto = require("node:crypto"); var import_node_stream = require("node:stream"); var import_koishi2 = require("koishi"); var import_koishi_plugin_booru = require("koishi-plugin-booru"); // packages/pixiv/src/types.ts var PixivAppApi; ((PixivAppApi2) => { let SearchAIType; ((SearchAIType2) => { SearchAIType2[SearchAIType2["SHOW_AI"] = 0] = "SHOW_AI"; SearchAIType2[SearchAIType2["HIDE_AI"] = 1] = "HIDE_AI"; })(SearchAIType = PixivAppApi2.SearchAIType || (PixivAppApi2.SearchAIType = {})); })(PixivAppApi || (PixivAppApi = {})); // packages/pixiv/src/utils.tsx var import_koishi = require("koishi"); var import_jsx_runtime = require("@satorijs/element/jsx-runtime"); function normaliseCaption(caption) { if (!(caption == null ? void 0 : caption.trim())) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("text", {}); } return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: import_koishi.h.transform(import_koishi.h.parse(caption), { a(attrs) { var _a, _b, _c; let url = attrs["href"] || ""; if (url) { let m = null; if (m = /pixiv:\/\/users\/(?<id>\d+)/.exec(url), (_a = m == null ? void 0 : m.groups) == null ? void 0 : _a.id) { url = `https://www.pixiv.net/u/${m.groups.id}`; } else if (m = /pixiv:\/\/illusts\/(?<id>\d+)/.exec(url), (_b = m == null ? void 0 : m.groups) == null ? void 0 : _b.id) { url = `https://www.pixiv.net/i/${m.groups.id}`; } else if (m = /pixiv:\/\/novels\/(?<id>\d+)/.exec(url), (_c = m == null ? void 0 : m.groups) == null ? void 0 : _c.id) { url = `https://www.pixiv.net/novel/show.php?id=${m.groups.id}`; } } return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: url, children: attrs.children || [] }); } }) }); } __name(normaliseCaption, "normaliseCaption"); // packages/pixiv/src/index.ts var CLIENT_ID = "MOBrBDS8blbauoSck0ZfDbtuzpyT"; var CLIENT_SECRET = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj"; var PixivImageSource = class extends import_koishi_plugin_booru.ImageSource { languages = ["en", "zh", "zh-CN", "zh-TW", "ja", "ko"]; source = "pixiv"; userId; accessToken; refreshToken; refreshTime; logger = this.ctx.logger("booru-pixiv"); constructor(ctx, config) { var _a; super(ctx, config); this.refreshToken = config.token; if (config.bypassMethod === "route") { if (!config.route || !((_a = this.ctx.server) == null ? void 0 : _a.selfUrl)) { throw new Error('route and selfUrl are required for bypass method "route".'); } if (!config.aesKey) { const aesKey = (0, import_node_crypto.randomBytes)(32).toString("hex"); config.aesKey = aesKey; this.ctx.setTimeout(() => ctx.scope.update(config, false), 0); this.logger.info("Found empty aesKey with a bypass method set to 'route', generated a random one in config."); } this.ctx.server.get((0, import_koishi2.trimSlash)(config.route) + "/:url(.+)", async (ctx2, next) => { const url = ctx2.request.url.replace(/^\/booru\/pixiv\/proxy\//, ""); const decrypted = Cipher.decrypt(decodeURIComponent(url), config.aesKey); if (typeof decrypted !== "string" || !decrypted.startsWith("https://i.pximg.net/")) return next(); const file = await this.http(decrypted, { headers: { Referer: "https://www.pixiv.net/" }, responseType: "stream" }); ctx2.set(Object.fromEntries(file.headers.entries())); ctx2.remove("Content-Length"); ctx2.response.status = file.status; ctx2.response.message = file.statusText; ctx2.body = import_node_stream.Readable.fromWeb(file.data); return next(); }); } } tokenize(query) { return query.split(/\s+/); } async get(query) { var _a; try { const data = await (query.raw.length ? this.search(query.tags.join(" ")) : this.recommend()); return Promise.all( import_koishi2.Random.shuffle( data.illusts.filter((illust) => illust.total_bookmarks > this.config.minBookmarks).filter((illust) => illust.x_restrict <= this.config.rank).filter((illust) => illust.illust_ai_type <= this.config.ai) ).slice(0, query.count).map(async (illust) => { let url = ""; if (illust.page_count > 1) { url = illust.meta_pages[0].image_urls.original; } else { url = illust.meta_single_page.original_image_url; } return { urls: { original: await this._handleImage(url) }, title: illust.title, pageUrl: `https://pixiv.net/i/${illust.id}`, author: illust.user.name, authorUrl: `https://pixiv.net/u/${illust.user.id}`, desc: normaliseCaption(illust.caption), tags: illust.tags.map((tag) => tag.name), nsfw: illust.x_restrict >= 1 }; }) ); } catch (err) { if (import_koishi2.HTTP.Error.is(err)) { throw new Error(`get pixiv image failed: ${err.message} (${(_a = err.response) == null ? void 0 : _a.status})`); } else { throw new Error("get pixiv image failed: " + err); } } } async search(keyword) { const url = "/v1/search/illust"; const params = { word: keyword, search_target: this.config.target, search_ai_type: this.config.ai === 2 ? PixivAppApi.SearchAIType.SHOW_AI : PixivAppApi.SearchAIType.HIDE_AI, sort: this.config.sort, duration: this.config.duration !== "all" && this.config.duration !== "custom" ? this.config.duration : void 0, min_bookmarks: this.config.minBookmarks || void 0, max_bookmarks: this.config.maxBookmarks || void 0, filter: "for_ios" }; Object.keys(params).forEach((key) => { if (params[key] === void 0) { delete params[key]; } }); if (!this.accessToken) { await this._login(); } return await this.http.get((0, import_koishi2.trimSlash)(this.config.endpoint) + url, { params, headers: this._getHeaders() }); } async recommend() { const url = "/v1/illust/recommended"; if (!this.accessToken) { await this._login(); } return await this.http.get((0, import_koishi2.trimSlash)(this.config.endpoint) + url, { params: { content_type: "illust", include_ranking_label: true, filter: "for_ios" }, headers: this._getHeaders() }); } async _login() { var _a; const endpoint = "https://oauth.secure.pixiv.net/"; const url = (0, import_koishi2.trimSlash)(endpoint) + "/auth/token"; const data = new URLSearchParams({ get_secure_url: "true", client_id: CLIENT_ID, client_secret: CLIENT_SECRET, grant_type: "refresh_token", refresh_token: this.refreshToken }); try { const resp = await this.http.post(url, data, { headers: { ...this._getHeaders(), "Content-Type": "application/x-www-form-urlencoded", "host": "oauth.secure.pixiv.net" }, validateStatus: (status) => [200, 301, 302].includes(status) }); this.userId = resp.user.id; this.accessToken = resp.access_token; this.refreshToken = resp.refresh_token; if (this.refreshTime) clearTimeout(this.refreshTime); this.refreshTime = setTimeout(() => this.accessToken = void 0, resp.expires_in * 1e3); return this.accessToken; } catch (err) { if (import_koishi2.HTTP.Error.is(err)) { throw new Error("Login failed with status code " + ((_a = err.response) == null ? void 0 : _a.status) + "\n" + JSON.stringify(err.response)); } else { throw new Error("Login failed with unknown error: " + err.message); } } } _getHeaders() { const headers = { "app-os": "ios", "app-os-version": "14.6", "user-agent": "PixivIOSApp/7.13.3 (iOS 14.6; iPhone13,2)" }; if (this.refreshToken && this.accessToken) { headers.Authorization = "Bearer " + this.accessToken; } return headers; } async _handleImage(url) { if (this.config.bypassMethod === "proxy" && this.config.proxy) { const proxy = typeof this.config.proxy === "string" ? this.config.proxy : this.config.proxy.endpoint; return url.replace(/^https?:\/\/i\.pximg\.net/, (0, import_koishi2.trimSlash)(proxy)); } else if (this.config.bypassMethod === "route" && this.config.route && this.ctx.get("server")) { const encrypted = Cipher.encrypt(url, this.config.aesKey); return (0, import_koishi2.trimSlash)(this.ctx.server.config.selfUrl) + (0, import_koishi2.trimSlash)(this.config.route) + "/" + encodeURIComponent(encrypted); } else if (this.config.bypassMethod === "asset" && this.ctx.get("assets")) { const filename = url.split("/").pop().split("?")[0]; const file = await this.http(url, { headers: { Referer: "https://www.pixiv.net/" } }); const base64 = Buffer.from(file.data).toString("base64"); return this.ctx.assets.upload(`data:${file.headers["content-type"]};base64,${base64}`, filename); } else { this.logger.warn( `Bypass method is set to ${this.config.bypassMethod}, but there's no candidate to handle the image.` ); return url; } } }; __name(PixivImageSource, "PixivImageSource"); __publicField(PixivImageSource, "inject", { required: ["booru"], optional: ["assets", "server"] }); ((PixivImageSource2) => { PixivImageSource2.Config = import_koishi2.Schema.intersect([ import_koishi_plugin_booru.ImageSource.createSchema({ label: "pixiv" }), import_koishi2.Schema.intersect([ import_koishi2.Schema.object({ endpoint: import_koishi2.Schema.string().default("https://app-api.pixiv.net/"), token: import_koishi2.Schema.string().required().role("secret"), target: import_koishi2.Schema.union([ import_koishi2.Schema.const("partial_match_for_tags"), import_koishi2.Schema.const("exact_match_for_tags"), import_koishi2.Schema.const("title_and_caption") ]).default("partial_match_for_tags"), sort: import_koishi2.Schema.union([import_koishi2.Schema.const("date_desc"), import_koishi2.Schema.const("date_asc"), import_koishi2.Schema.const("popular_desc")]).default( "date_desc" ), duration: import_koishi2.Schema.union([ import_koishi2.Schema.const("within_last_day"), import_koishi2.Schema.const("within_last_week"), import_koishi2.Schema.const("within_last_month"), import_koishi2.Schema.const("all"), import_koishi2.Schema.const("custom").disabled() ]).default("all"), minBookmarks: import_koishi2.Schema.number().default(0), maxBookmarks: import_koishi2.Schema.number().default(0), rank: import_koishi2.Schema.union([import_koishi2.Schema.const(0), import_koishi2.Schema.const(1), import_koishi2.Schema.const(2)]).default(0), ai: import_koishi2.Schema.union([import_koishi2.Schema.const(1), import_koishi2.Schema.const(2)]).default(1) }), import_koishi2.Schema.intersect([ import_koishi2.Schema.object({ bypassMethod: import_koishi2.Schema.union([import_koishi2.Schema.const("proxy"), import_koishi2.Schema.const("route"), import_koishi2.Schema.const("asset")]).default( "proxy" ) }), import_koishi2.Schema.union([ import_koishi2.Schema.object({ bypassMethod: import_koishi2.Schema.const("proxy"), proxy: import_koishi2.Schema.union([ import_koishi2.Schema.const("https://i.pixiv.re"), import_koishi2.Schema.const("https://i.pixiv.cat"), import_koishi2.Schema.const("https://i.pixiv.nl"), import_koishi2.Schema.object({ endpoint: import_koishi2.Schema.string().required() }) ]).default("https://i.pixiv.re") }), import_koishi2.Schema.object({ bypassMethod: import_koishi2.Schema.const("route"), route: import_koishi2.Schema.string().default("/booru/pixiv/proxy"), aesKey: import_koishi2.Schema.string().hidden().default("") }), import_koishi2.Schema.object({ bypassMethod: import_koishi2.Schema.const("asset") }) ]) ]) ]).i18n({ "zh-CN": require_zh_CN_schema() }) ]); })(PixivImageSource || (PixivImageSource = {})); var Cipher = class { static encrypt(data, key) { try { const iv = (0, import_node_crypto.randomBytes)(16); const cipher = (0, import_node_crypto.createCipheriv)("aes-256-cbc", Buffer.from(key, "hex"), iv); const encrypted = Buffer.concat([iv, cipher.update(data), cipher.final()]); return encrypted.toString("base64"); } catch (err) { return null; } } static decrypt(data, key) { try { const encrypted = Buffer.from(data, "base64"); const iv = encrypted.slice(0, 16); const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-cbc", Buffer.from(key, "hex"), iv); const decrypted = Buffer.concat([decipher.update(encrypted.slice(16)), decipher.final()]); return decrypted.toString(); } catch (err) { return null; } } }; __name(Cipher, "Cipher"); var src_default = PixivImageSource; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = {});