koishi-plugin-booru
Version:
Image service for Koishi
542 lines (536 loc) • 23.2 kB
JavaScript
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.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": "获取图片失败", output: { desc: "{0}", author: "作者: {0}", homepage: "作者主页: {0}", link: "页面地址: {0}", source: "图源: {0}", tags: "标签: {0}" }, tips: "<b>提示:</b>{0}" } } }, booru: { tips: { general: ["不带参数调用此命令将会随机返回一张图片", "可以使用 <code>-l <label></code> 或 <code>--label <label></code> 参数指定图源标签", "可以使用 <code>-c <count></code> 或 <code>--count <count></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("booru.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;
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,
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) => config.nsfw || !image.nsfw);
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 = await ctx.booru.imgUrlToAssetUrl(url);
if (!url) {
children.unshift(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("i18n", {
path: "commands.booru.messages.no-image"
}));
continue;
}
} else if (config.base64) {
url = await ctx.booru.imgUrlToBase64(url);
if (!url) {
children.unshift(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("i18n", {
path: "commands.booru.messages.no-image"
}));
continue;
}
}
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).description("图源标签,可用于在指令中手动指定图源。"),
weight: import_koishi2.Schema.number().min(1).default(1).description("图源权重。在多个符合标签的图源中,将按照各自的权重随机选择。")
}).description("全局设置"),
import_koishi2.Schema.object({
proxyAgent: import_koishi2.Schema.string().default(void 0).description("请求图片时使用代理服务器。")
}).description("请求设置")
]);
}
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.Quester.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.Quester.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.Quester.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).description("自动检测输入语言并选择语言匹配的图源。")
}),
import_koishi3.Schema.object({
detectLanguage: import_koishi3.Schema.const(true).description("自动检测输入语言并选择语言匹配的图源。"),
confidence: import_koishi3.Schema.number().default(0.5).description("语言检测的置信度。")
})
]),
import_koishi3.Schema.object({
maxCount: import_koishi3.Schema.number().default(10).description("每次搜索的最大数量。"),
nsfw: import_koishi3.Schema.boolean().default(false).description("是否允许输出 NSFW 内容。")
})
]).description("搜索设置"),
import_koishi3.Schema.object({
output: import_koishi3.Schema.union([
import_koishi3.Schema.const(0).description("仅发送图片"),
import_koishi3.Schema.const(1).description("发送图片和相关信息"),
import_koishi3.Schema.const(2).description("发送图片、相关信息和链接"),
import_koishi3.Schema.const(3).description("发送全部信息")
]).description("输出方式。").default(1),
outputMethod: import_koishi3.Schema.union([
import_koishi3.Schema.const("one-by-one").description("逐条发送每张图片"),
import_koishi3.Schema.const("merge-multiple").description("合并多条发送 (部分平台可能不支持)"),
import_koishi3.Schema.const("forward-all").description("合并为子话题发送所有图片 (部分平台需求较高权限)"),
import_koishi3.Schema.const("forward-multiple").description("仅当多于一张图片使用合并为子话题发送 (部分平台需求较高权限)")
]).experimental().role("radio").default("merge-multiple").description("发送方式。"),
preferSize: import_koishi3.Schema.union([
import_koishi3.Schema.const("original").description("原始尺寸"),
import_koishi3.Schema.const("large").description("较大尺寸 (通常为约 1200px)"),
import_koishi3.Schema.const("medium").description("中等尺寸 (通常为约 600px)"),
import_koishi3.Schema.const("small").description("较小尺寸 (通常为约 300px)"),
import_koishi3.Schema.const("thumbnail").description("缩略图")
]).description("优先使用图片的最大尺寸。").default("large"),
autoResize: import_koishi3.Schema.computed(import_koishi3.Schema.boolean()).default(false).description("根据 preferSize 自动缩小过大的图片。<br/> - 需要安装提供 canvas 服务的插件"),
asset: import_koishi3.Schema.boolean().default(false).description("优先使用 [assets服务](https://assets.koishi.chat/) 转存图片。"),
base64: import_koishi3.Schema.boolean().default(false).description("使用 base64 发送图片。"),
spoiler: import_koishi3.Schema.union([
import_koishi3.Schema.const(0).description("禁用"),
import_koishi3.Schema.const(1).description("所有图片"),
import_koishi3.Schema.const(2).description("仅 NSFW 图片")
]).description("发送为隐藏图片,单击后显示(在 QQ 平台中以「合并转发」发送)。").default(0).experimental(),
showTips: import_koishi3.Schema.boolean().default(true).description("是否输出使用提示信息。")
}).description("输出设置")
]);
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
});