UNPKG

koishi-plugin-booru

Version:
551 lines (543 loc) 23.8 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; 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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); 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/core/src/locales/zh-CN.source.schema.yml var require_zh_CN_source_schema = __commonJS({ "packages/core/src/locales/zh-CN.source.schema.yml"(exports, module2) { module2.exports = { $inner: [{ $description: "全局设置", label: "图源标签,可用于在指令中手动指定图源。", weight: "图源权重。在多个符合标签的图源中,将按照各自的权重随机选择。" }, { $description: "请求设置", proxyAgent: "请求图片时使用代理服务器。" }] }; } }); // packages/core/src/locales/zh-CN.schema.yml var require_zh_CN_schema = __commonJS({ "packages/core/src/locales/zh-CN.schema.yml"(exports, module2) { module2.exports = { $inner: [{ $description: "搜索设置", detectLanguage: "自动检测输入语言并选择语言匹配的图源。", confidence: "语言检测的置信度。", maxCount: "每次搜索的最大数量。", nsfw: "是否允许输出 NSFW 内容。", blacklist: "黑名单列表,一旦匹配到黑名单中的关键词,将不会发送图片。" }, { $description: "输出设置", output: { $description: "输出方式。", $inner: ["仅发送图片", "发送图片和相关信息", "发送图片、相关信息和链接", "发送全部信息"] }, outputMethod: { $description: "发送方式。", $inner: ["逐条发送每张图片", "合并多条发送 (部分平台可能不支持)", "合并为子话题发送所有图片 (部分平台需求较高权限)", "仅当多于一张图片使用合并为子话题发送 (部分平台需求较高权限)"] }, preferSize: { $description: "优先使用图片的最大尺寸。", $inner: ["原始尺寸", "较大尺寸 (通常为约 1200px)", "中等尺寸 (通常为约 600px)", "较小尺寸 (通常为约 300px)", "缩略图"] }, autoResize: { $description: "根据 preferSize 自动缩小过大的图片。 **需要安装提供 canvas 服务的插件**" }, asset: "优先使用 [assets服务](https://assets.koishi.chat/) 转存图片。", base64: "使用 base64 发送图片。", spoiler: { $description: "发送为隐藏图片,单击后显示(在 QQ 平台中以「合并转发」发送)。", $inner: ["禁用", "所有图片", "仅 NSFW 图片"] }, showTips: "是否输出使用提示信息。" }] }; } }); // packages/core/src/locales/zh-CN.yml var require_zh_CN = __commonJS({ "packages/core/src/locales/zh-CN.yml"(exports, module2) { module2.exports = { commands: { booru: { description: "搜索图片", options: { label: "指定图源标签" }, messages: { "no-source": "当前未找到可用图源。请从插件市场添加 booru 系图源插件并启用 (插件名通常以 booru- 开头)。", "no-result": "没有找到符合条件的图片", "no-image": "获取图片失败", "count-invalid": "指定的数量过多或无效。", output: { desc: "{0}", author: "作者: {0}", homepage: "作者主页: {0}", link: "页面地址: {0}", source: "图源: {0}", tags: "标签: {0}" }, tips: "<b>提示:</b>{0}" } } }, booru: { tips: { general: ["不带参数调用此命令将会随机返回一张图片", "可以使用 <code>-l &lt;label&gt;</code> 或 <code>--label &lt;label&gt;</code> 参数指定图源标签", "可以使用 <code>-c &lt;count&gt;</code> 或 <code>--count &lt;count&gt;</code> 参数指定返回图片数量", "使用 <code>{0} -c 3</code> 可以返回 3 张图片", "指定的图片数量与实际返回的图片数量可能有所不同"] } } }; } }); // packages/core/src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config2, ImageSource: () => ImageSource, OutputType: () => OutputType, SpoilerType: () => SpoilerType, apply: () => apply2, preferSizes: () => preferSizes, sizeNameToFixedWidth: () => sizeNameToFixedWidth }); module.exports = __toCommonJS(src_exports); var import_koishi3 = require("koishi"); var import_languagedetect = __toESM(require("languagedetect")); // packages/core/src/command.tsx var command_exports = {}; __export(command_exports, { apply: () => apply, inject: () => inject }); var import_koishi = require("koishi"); var import_jsx_runtime = require("@satorijs/element/jsx-runtime"); var inject = { required: ["booru"], optional: ["assets"] }; function apply(ctx, config) { const getTips = /* @__PURE__ */ __name((session) => { var _a, _b; for (const locale of ctx.i18n.fallback([ ...((_a = session.user) == null ? void 0 : _a.locales) || [], ...((_b = session.channel) == null ? void 0 : _b.locales) || [] ])) { if (ctx.i18n._data[locale]) { const tips = Object.keys(ctx.i18n._data[locale] || {}).filter((k) => k.startsWith("booru.tips")); if (tips.length) return tips; } } }, "getTips"); const count = /* @__PURE__ */ __name((value, session) => { const count2 = parseInt(value); if (count2 < 1 || count2 > config.maxCount) { session.send(session.text("commands.booru.messages.count-invalid")); return 1; } return count2; }, "count"); const command = ctx.command("booru <query:text>").option("count", "-c <count:number>", { type: count, fallback: 1 }).option("label", "-l <label:string>").action(async ({ session, options }, query) => { var _a, _b, _c, _d, _e, _f, _g, _h; if (!ctx.booru.hasSource(options.label)) return session.text(".no-source"); query = (_a = query == null ? void 0 : query.trim()) != null ? _a : ""; if (query) { const countMatch = /(-c|--count)\s+(\d+)/g.exec(query); if (countMatch) { options.count = count(countMatch[2], session); query = query.replace(countMatch[0], "").trim(); } const labelMatch = /(-l|--label)\s+([^\s]+)/g.exec(query); if (labelMatch) { options.label = labelMatch[2]; query = query.replace(labelMatch[0], "").trim(); } } const images = await ctx.booru.get({ query, count: options.count || 1, labels: (_e = (_d = (_c = (_b = options.label) == null ? void 0 : _b.split(",")) == null ? void 0 : _c.map((x) => x.trim())) == null ? void 0 : _d.filter(Boolean)) != null ? _e : [] }); const source = images == null ? void 0 : images.source; const filtered = images == null ? void 0 : images.filter((image) => { var _a2, _b2; if (config.nsfw && image.nsfw) return false; if (((_a2 = config.blacklist) == null ? void 0 : _a2.length) && ((_b2 = image.tags) == null ? void 0 : _b2.length)) { for (const tag of image.tags) { if (config.blacklist.includes(tag)) return false; } } return true; }); if (!(filtered == null ? void 0 : filtered.length)) return session == null ? void 0 : session.text("commands.booru.messages.no-result"); const output = []; for (const image of filtered) { const children = []; let url = ""; for (const size of preferSizes.slice(preferSizes.indexOf(config.preferSize))) { url = (_f = image.urls) == null ? void 0 : _f[size]; if (url) { break; } } url || (url = image.url); if (session.resolve(config.autoResize) && sizeNameToFixedWidth[config.preferSize]) { url = await ctx.booru.resizeImageToFixedWidth(url, sizeNameToFixedWidth[config.preferSize]); } if (config.asset && ctx.assets) { url = (_g = await ctx.booru.imgUrlToAssetUrl(url)) != null ? _g : url; } else if (config.base64) { url = (_h = await ctx.booru.imgUrlToBase64(url)) != null ? _h : url; } switch (config.output) { case 3 /* All */: if (image.tags) { children.unshift( /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i18n", { path: "commands.booru.messages.output.source", children: [source] }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i18n", { path: "commands.booru.messages.output.tags", children: [image.tags.join(", ")] }) }) ); } case 2 /* ImageAndLink */: case 1 /* ImageAndInfo */: if (image.title || image.author || image.desc) { children.unshift( /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: config.output >= 2 /* ImageAndLink */ && image.pageUrl ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: image.pageUrl, children: image.title }) : image.title }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: config.output >= 2 /* ImageAndLink */ && image.authorUrl ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: image.authorUrl, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i18n", { path: "commands.booru.messages.output.author", children: [image.author] }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i18n", { path: "commands.booru.messages.output.author", children: [image.author] }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i18n", { path: "commands.booru.messages.output.desc", children: [image.desc] }) }) ); } case 0 /* ImageOnly */: children.unshift( /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { spoiler: (() => { switch (config.spoiler) { case 0 /* Disabled */: return false; case 1 /* All */: return true; case 2 /* OnlyNSFW */: return Boolean(image.nsfw); } })(), src: url }) ); } output.push(children); } if (config.showTips) { const tips = getTips(session); if (tips) { const tip = import_koishi.Random.pick(tips); output.push( /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i18n", { path: "commands.booru.messages.tips" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i18n", { path: tip, children: [command.displayName] }) ] }) ); } } switch (session.resolve(config.outputMethod)) { case "one-by-one": return output.map((children) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children })); case "merge-multiple": return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: output.map((children) => children) }); case "forward-all": return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("message", { forward: true, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("author", { id: session.userId, name: session.username }), output.map((children) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children })) ] }); case "forward-multiple": if (output.length === 1) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: output[0] }); return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("message", { forward: true, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("author", { id: session.userId, name: session.username }), output.map((children) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children })) ] }); } }); } __name(apply, "apply"); // packages/core/src/source.ts var import_koishi2 = require("koishi"); var ImageSource = class { constructor(ctx, config) { this.ctx = ctx; this.config = config; this.ctx.booru.register(this); this.http = config.proxyAgent ? ctx.http.extend({ proxyAgent: config.proxyAgent }) : ctx.http; } languages = []; source; http; tokenize(query) { return query.split(",").map((x) => x.trim()).filter(Boolean).map((x) => x.toLowerCase().replace(/\s+/g, "_")); } }; __name(ImageSource, "ImageSource"); __publicField(ImageSource, "inject", ["booru"]); ((ImageSource2) => { function createSchema(o) { return import_koishi2.Schema.intersect([ import_koishi2.Schema.object({ label: import_koishi2.Schema.string().default(o.label), weight: import_koishi2.Schema.number().min(1).default(1) }), import_koishi2.Schema.object({ proxyAgent: import_koishi2.Schema.string().default(void 0) }) ]).i18n({ "zh-CN": require_zh_CN_source_schema() }); } ImageSource2.createSchema = createSchema; __name(createSchema, "createSchema"); ImageSource2.Config = createSchema({ label: "default" }); let PreferSize; ((PreferSize2) => { PreferSize2["Original"] = "original"; PreferSize2["Large"] = "large"; PreferSize2["Medium"] = "medium"; PreferSize2["Small"] = "small"; PreferSize2["Thumbnail"] = "thumbnail"; })(PreferSize = ImageSource2.PreferSize || (ImageSource2.PreferSize = {})); })(ImageSource || (ImageSource = {})); var preferSizes = ["thumbnail", "large", "medium", "small", "original"]; var sizeNameToFixedWidth = { thumbnail: 128, small: 320, medium: 640, large: 1280 }; // packages/core/src/index.ts var logger = new import_koishi3.Logger("booru"); var ImageService = class extends import_koishi3.Service { sources = []; languageDetect = new import_languagedetect.default(); constructor(ctx, config) { super(ctx, "booru", true); this.config = config; } register(source) { return this[import_koishi3.Context.origin].effect(() => { this.sources.push(source); return () => (0, import_koishi3.remove)(this.sources, source); }); } hasSource(name) { if (name) { return this.sources.some((source) => source.config.label === name); } return this.sources.some(Boolean); } async get(query) { const sources = this.sources.filter((source) => { if (query.labels.length && !query.labels.includes(source.config.label)) return false; if (this.config.detectLanguage) { const probabilities = this.languageDetect.detect(query.query, 3).filter((x) => x[1] > this.config.confidence); if (!probabilities.length) { return true; } return probabilities.some(([lang]) => source.languages.includes(lang)); } return true; }).sort((a, b) => { if (a.config.weight !== b.config.weight) return b.config.weight - a.config.weight; return Math.random() - 0.5; }); for (const source of sources) { const tags = source.tokenize(query.query); const images = await source.get({ count: query.count, tags, raw: query.query }).catch((err) => { var _a, _b, _c; if (import_koishi3.HTTP.Error.is(err)) { logger.warn( [ `source ${source.config.label} request failed`, ((_a = err.response) == null ? void 0 : _a.status) ? `with code ${(_b = err.response) == null ? void 0 : _b.status} ${JSON.stringify((_c = err.response) == null ? void 0 : _c.data)}` : "" ].join(" ") ); } else { logger.error(`source ${source.config.label} unknown error: ${err.message}`); } logger.debug(err); return []; }); if (images == null ? void 0 : images.length) { return Object.assign(images, { source: source.source }); } } return void 0; } async resizeImageToFixedWidth(url, size) { if (!size || size < 0) { return url; } if (!this.ctx.canvas) { logger.warn("Canvas service is not available, thus cannot resize image now."); return url; } const resp = await this.ctx.http(url, { method: "GET", responseType: "arraybuffer", proxyAgent: "" }).catch((err) => { var _a, _b; if (import_koishi3.HTTP.Error.is(err)) { logger.warn( `Request images failed with HTTP status ${(_a = err.response) == null ? void 0 : _a.status}: ${JSON.stringify((_b = err.response) == null ? void 0 : _b.data)}.` ); } else { logger.error(`Request images failed with unknown error: ${err.message}.`); } return null; }); if (!(resp == null ? void 0 : resp.data)) { return url; } const buffer = Buffer.from(resp.data); url = `data:${resp.headers.get("content-type")};base64,${buffer.toString("base64")}`; try { const img = await this.ctx.canvas.loadImage(buffer); let width = img.naturalWidth; let height = img.naturalHeight; const ratio = size / Math.max(width, height); if (ratio < 1) { width = Math.floor(width * ratio); height = Math.floor(height * ratio); const canvas = await this.ctx.canvas.createCanvas(width, height); const ctx2d = canvas.getContext("2d"); ctx2d.drawImage(img, 0, 0, width, height); url = await canvas.toDataURL("image/png"); if (typeof canvas.dispose === "function") { await canvas.dispose(); } } if (typeof img.dispose === "function") { await img.dispose(); } return url; } catch (err) { logger.error(`Resize image failed with error: ${err.message}.`); return url; } } async imgUrlToAssetUrl(url) { return await this.ctx.assets.upload(url, Date.now().toString()).catch(() => { logger.warn("Request failed when trying to store image with assets service."); return null; }); } async imgUrlToBase64(url) { return this.ctx.http(url, { method: "GET", responseType: "arraybuffer", proxyAgent: "" }).then((resp) => { return `data:${resp.headers["content-type"]};base64,${Buffer.from(resp.data).toString("base64")}`; }).catch((err) => { var _a, _b; if (import_koishi3.HTTP.Error.is(err)) { logger.warn( `Request images failed with HTTP status ${(_a = err.response) == null ? void 0 : _a.status}: ${JSON.stringify((_b = err.response) == null ? void 0 : _b.data)}.` ); } else { logger.error(`Request images failed with unknown error: ${err.message}.`); } return null; }); } }; __name(ImageService, "ImageService"); __publicField(ImageService, "inject", { required: [], optional: ["assets", "canvas"] }); var OutputType = /* @__PURE__ */ ((OutputType2) => { OutputType2[OutputType2["ImageOnly"] = 0] = "ImageOnly"; OutputType2[OutputType2["ImageAndInfo"] = 1] = "ImageAndInfo"; OutputType2[OutputType2["ImageAndLink"] = 2] = "ImageAndLink"; OutputType2[OutputType2["All"] = 3] = "All"; return OutputType2; })(OutputType || {}); var SpoilerType = /* @__PURE__ */ ((SpoilerType2) => { SpoilerType2[SpoilerType2["Disabled"] = 0] = "Disabled"; SpoilerType2[SpoilerType2["All"] = 1] = "All"; SpoilerType2[SpoilerType2["OnlyNSFW"] = 2] = "OnlyNSFW"; return SpoilerType2; })(SpoilerType || {}); var Config2 = import_koishi3.Schema.intersect([ import_koishi3.Schema.intersect([ import_koishi3.Schema.union([ import_koishi3.Schema.object({ detectLanguage: import_koishi3.Schema.boolean().default(false) }), import_koishi3.Schema.object({ detectLanguage: import_koishi3.Schema.const(true), confidence: import_koishi3.Schema.number().default(0.5) }) ]), import_koishi3.Schema.object({ maxCount: import_koishi3.Schema.number().default(10), nsfw: import_koishi3.Schema.boolean().default(false), blacklist: import_koishi3.Schema.array(import_koishi3.Schema.string()).default([]) }) ]), import_koishi3.Schema.object({ output: import_koishi3.Schema.union([import_koishi3.Schema.const(0), import_koishi3.Schema.const(1), import_koishi3.Schema.const(2), import_koishi3.Schema.const(3)]).default(1), outputMethod: import_koishi3.Schema.union([ import_koishi3.Schema.const("one-by-one"), import_koishi3.Schema.const("merge-multiple"), import_koishi3.Schema.const("forward-all"), import_koishi3.Schema.const("forward-multiple") ]).experimental().role("radio").default("merge-multiple"), preferSize: import_koishi3.Schema.union([ import_koishi3.Schema.const("original"), import_koishi3.Schema.const("large"), import_koishi3.Schema.const("medium"), import_koishi3.Schema.const("small"), import_koishi3.Schema.const("thumbnail") ]).default("large"), autoResize: import_koishi3.Schema.computed(import_koishi3.Schema.boolean()).experimental().default(false), asset: import_koishi3.Schema.boolean().default(false), base64: import_koishi3.Schema.boolean().default(false), spoiler: import_koishi3.Schema.union([import_koishi3.Schema.const(0), import_koishi3.Schema.const(1), import_koishi3.Schema.const(2)]).default(0).experimental(), showTips: import_koishi3.Schema.boolean().default(true) }) ]).i18n({ "zh-CN": require_zh_CN_schema() }); function apply2(ctx, config) { ctx.plugin(ImageService, config); ctx.plugin(command_exports, config); ctx.i18n.define("zh", require_zh_CN()); } __name(apply2, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, ImageSource, OutputType, SpoilerType, apply, preferSizes, sizeNameToFixedWidth });