UNPKG

koishi-plugin-memes-api

Version:

表情包制作插件调用 API 版

1,269 lines (1,239 loc) 49.6 kB
"use strict"; 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 __name = (target, value) => __defProp(target, "name", { value, configurable: true }); 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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.tsx var index_exports = {}; __export(index_exports, { Config: () => Config, apply: () => apply11, inject: () => inject, name: () => name, usage: () => usage }); module.exports = __toCommonJS(index_exports); var import_meme_generator_rs_api3 = require("meme-generator-rs-api"); // src/commands/generate.ts var import_koishi2 = require("koishi"); // src/utils/error.ts var import_meme_generator_rs_api = require("meme-generator-rs-api"); // src/utils/arg-parse.ts var ArgSyntaxError = class _ArgSyntaxError extends SyntaxError { constructor(type, char, index) { const message = (() => { switch (type) { case _ArgSyntaxError.Type.UnexpectedChar: return `Unexpected char ${char} in input string at index ${index}, consider use backslash to escape`; case _ArgSyntaxError.Type.UnterminatedQuote: return `Unterminated quote ${char} in input string at index ${index}`; } })(); super(message); this.type = type; this.char = char; this.index = index; } static { __name(this, "ArgSyntaxError"); } }; ((ArgSyntaxError2) => { let Type; ((Type2) => { Type2[Type2["UnexpectedChar"] = 0] = "UnexpectedChar"; Type2[Type2["UnterminatedQuote"] = 1] = "UnterminatedQuote"; })(Type = ArgSyntaxError2.Type || (ArgSyntaxError2.Type = {})); function getI18NKey(e) { const kPfx = `memes-api.errors.syntax-error.`; switch (e.type) { case 0 /* UnexpectedChar */: return `${kPfx}unexpected-char`; case 1 /* UnterminatedQuote */: return `${kPfx}unterminated-quote`; } } ArgSyntaxError2.getI18NKey = getI18NKey; __name(getI18NKey, "getI18NKey"); })(ArgSyntaxError || (ArgSyntaxError = {})); var quotePairs = { '"': '"', "'": "'", "`": "`", "“": "”", "‘": "’" }; var quotes = [...new Set(Object.entries(quotePairs).flat())]; function splitArgString(argString) { const args = []; const currentArgChars = []; let inQuote = null; let outQuote = null; let escapeNext = false; let lastInQuoteIndex = -1; for (let i = 0; i < argString.length; i += 1) { const char = argString[i]; if (escapeNext) { currentArgChars.push(char); escapeNext = false; continue; } if (char === "\\") { escapeNext = true; continue; } if (inQuote) { if (char === outQuote) { inQuote = null; outQuote = null; } else if (char === inQuote) { throw new ArgSyntaxError(0 /* UnexpectedChar */, char, i); } else { currentArgChars.push(char); } continue; } if (char in quotePairs) { inQuote = char; outQuote = quotePairs[char]; lastInQuoteIndex = i; } else if (/^\s$/.test(char)) { if (currentArgChars.length) { args.push(currentArgChars.join("")); currentArgChars.length = 0; } } else { currentArgChars.push(char); } } if (currentArgChars.length) args.push(currentArgChars.join("")); if (inQuote) { throw new ArgSyntaxError( 1 /* UnterminatedQuote */, inQuote, lastInQuoteIndex ); } return args; } __name(splitArgString, "splitArgString"); function escapeArgs(args, extraShouldQuote) { return args.map((arg) => { const needQuote = (() => { for (const q of quotes) if (arg.includes(q)) return true; if (extraShouldQuote) { for (const q of extraShouldQuote) if (arg.includes(q)) return true; } return false; })(); if (!needQuote) return arg; for (const q of quotes) arg.replaceAll(q, `\\${q}`); return `"${arg}"`; }).join(" "); } __name(escapeArgs, "escapeArgs"); // src/utils/common.ts var import_koishi = require("koishi"); function checkInRange(value, min, max) { return value >= min && value <= max; } __name(checkInRange, "checkInRange"); function constructBlobFromFileResp(resp) { return new Blob([resp.data], { type: resp.type }); } __name(constructBlobFromFileResp, "constructBlobFromFileResp"); function formatRange(min, max) { return min === max ? min.toString() : `${min} ~ ${max}`; } __name(formatRange, "formatRange"); function formatKeywords(keywords) { return keywords.map((v) => `“${v}”`).join("、"); } __name(formatKeywords, "formatKeywords"); function listJoin(list, splitter) { const newList = []; for (const item of list) { newList.push(item); newList.push(splitter); } newList.pop(); return newList; } __name(listJoin, "listJoin"); function listFlatJoin(list, splitter) { const newList = []; for (let i = 0; i < list.length - 1; i += 1) { newList.push(...list[i]); newList.push(...splitter); } newList.push(...list[list.length - 1]); return newList; } __name(listFlatJoin, "listFlatJoin"); function transformRegex(pythonRegex) { return pythonRegex.replace(/\(\?P<(?<n>\w+?)>/g, "(?<$<n>>"); } __name(transformRegex, "transformRegex"); function extractContentPlaintext(content) { let el; try { el = import_koishi.h.parse(content); } catch (e) { return content; } const textBuffer = []; const visit = /* @__PURE__ */ __name((e) => { if (e.children.length) { for (const child of e.children) visit(child); } if (e.type === "text") { const t = e.attrs.content; if (t) textBuffer.push(t); } }, "visit"); for (const child of el) visit(child); return textBuffer.join(""); } __name(extractContentPlaintext, "extractContentPlaintext"); function replaceBracketVar(v, res) { return v.replace(/(?<l>[^\{])?\{(?<v>.+?)\}(?<r>[^\}])?/g, (...args) => { const { l, v: v2, r } = args[args.length - 1]; const index = parseInt(v2); let resolved; if (!isNaN(index)) { resolved = res[index] ?? v2; } else if (res.groups && v2 in res.groups) { resolved = res.groups[v2]; } else { resolved = v2; } return `${l ?? ""}${extractContentPlaintext(resolved)}${r ?? ""}`; }); } __name(replaceBracketVar, "replaceBracketVar"); function isVersionMeets(version, minVersion) { const parts = version.split(".").map((v) => parseInt(v, 10)); for (let i = 0; i < minVersion.length; i++) { const part = isNaN(parts[i]) ? 0 : parts[i] ?? 0; const minPart = minVersion[i] ?? 0; if (part < minPart) return false; } return true; } __name(isVersionMeets, "isVersionMeets"); // src/utils/user-info.ts var GetAvatarFailedError = class extends Error { constructor(platform, userId) { super(`Failed to get avatar for user ${userId} on platform ${platform}.`); this.platform = platform; this.userId = userId; this.name = "FetchAvatarFailedError"; } static { __name(this, "GetAvatarFailedError"); } }; async function apply(ctx, config) { ctx.$.getInfoFromID = async (session, userId, forceFallback) => { const platformSpecific = { onebot: /* @__PURE__ */ __name(async () => { const url = `http://q.qlogo.cn/headimg_dl?dst_uin=${userId}&spec=640`; const bot = session.bot; if (session.isDirect) { const data2 = await bot.internal.getStrangerInfo(userId); return { url, userInfo: { name: data2.nickname, gender: data2.sex || "unknown" } }; } const data = await bot.internal.getGroupMemberInfo(session.guildId, userId); return { url, userInfo: { name: data.card || data.nickname, gender: data.sex || "unknown" } }; }, "onebot") }; const fallback = /* @__PURE__ */ __name(async () => { if (session.event.user?.id === userId && session.event.user?.avatar) { return { url: session.event.user.avatar, userInfo: { name: session.username || session.userId || "", gender: "unknown" } }; } else if (session.bot.getUser) { const user = await session.bot.getUser(userId, session.guildId); if (user.avatar) { return { url: user.avatar, userInfo: { name: user.nick || user.name || "", gender: "unknown" } }; } } throw new TypeError( `User ${userId} in platform ${session.platform} has no avatar` ); }, "fallback"); const specificFunc = platformSpecific[session.platform]; const func = !specificFunc || forceFallback ? fallback : platformSpecific[session.platform]; try { return await func(); } catch (e) { ctx.logger.error(e); if (func !== fallback) { ctx.logger.warn( `Failed to get user info from platform specific method, falling back to universal` ); return ctx.$.getInfoFromID(session, userId, true); } throw new GetAvatarFailedError(session.platform, userId); } }; } __name(apply, "apply"); // src/utils/error.ts async function apply2(ctx, config) { ctx.$.handleMemeError = (session, e) => { if (e instanceof import_meme_generator_rs_api.MemeError.Detailed) { const { code, data } = e.data; switch (code) { case 510: return session.text("memes-api.errors.image-decode", [data.error]); case 520: return session.text("memes-api.errors.image-encode", [data.error]); case 530: return session.text("memes-api.errors.image-asset-missing", [data.path]); case 540: return session.text("memes-api.errors.deserialize", [data.error]); case 550: return session.text("memes-api.errors.image-number-mismatch", [ formatRange(data.min, data.max), data.actual ]); case 551: return session.text("memes-api.errors.text-number-mismatch", [ formatRange(data.min, data.max), data.actual ]); case 560: return session.text("memes-api.errors.text-over-length"); case 570: return session.text("memes-api.errors.meme-feedback", [data.feedback]); } } if (e.httpStatus === 404) { return session.text("memes-api.errors.no-such-meme"); } return session.text("memes-api.errors.other-error", [e.httpStatus, e.message]); }; ctx.$.handleResolveArgsError = (session, e) => { if (!(e instanceof ArgSyntaxError)) throw e; ctx.logger.warn(e.message); return config.silentShortcut && session.memesApi.shortcut ? void 0 : session.text(ArgSyntaxError.getI18NKey(e), e); }; ctx.$.handleResolveImagesAndInfosError = (session, e) => { if (e instanceof GetAvatarFailedError) { return config.silentShortcut && session.memesApi.shortcut && config.moreSilent ? void 0 : session.text("memes-api.errors.can-not-get-avatar", e); } ctx.logger.warn(e); return config.silentShortcut && session.memesApi.shortcut && config.moreSilent ? void 0 : session.text("memes-api.errors.download-image-failed"); }; ctx.$.handleRenderError = (session, e) => { ctx.logger.warn(e); if (!(e instanceof import_meme_generator_rs_api.MemeError)) throw e; return config.silentShortcut && session.memesApi.shortcut && (config.moreSilent || // or arg error e instanceof import_meme_generator_rs_api.MemeError.Detailed && [551, 552, 560].includes(e.data.code)) ? void 0 : ctx.$.handleMemeError(session, e); }; ctx.$.handleError = (session, e) => { ctx.logger.warn(e); if (e instanceof ArgSyntaxError) { return session.text(ArgSyntaxError.getI18NKey(e), e); } if (e instanceof import_meme_generator_rs_api.MemeError) { return ctx.$.handleMemeError(session, e); } throw e; }; } __name(apply2, "apply"); // src/utils/meme-manage.ts async function apply3(ctx, config) { ctx.$.infos = {}; ctx.$.apiVersion = "0.0.0"; ctx.$.fetchInfos = async () => { const infoArr = await ctx.$.api.getInfos(); return infoArr.reduce( (acc, info) => { acc[info.key] = info; return acc; }, {} ); }; ctx.$.updateInfos = async () => { ctx.$.apiVersion = await ctx.$.api.getVersion(); ctx.$.infos = await ctx.$.fetchInfos(); return ctx.$.infos; }; } __name(apply3, "apply"); // src/utils/index.ts async function apply4(ctx, config) { await apply3(ctx, config); await apply2(ctx, config); await apply(ctx, config); } __name(apply4, "apply"); // src/commands/generate.ts async function apply5(ctx, config) { const cmdGenerate = ctx.$.cmd.subcommand(".generate").action(async ({ session }) => { if (session?.memesApi.inGenerateSubCommand) return; return session?.execute("help meme.generate"); }); const generateSubCommands = []; ctx.$.resolveArgs = async (session, args) => { if (!session.userId) { throw new TypeError("userId not found in session"); } const imageInfos = []; const texts = []; const names = []; if (session.quote?.elements) { const visit2 = /* @__PURE__ */ __name((e) => { if (e.children.length) { for (const child of e.children) visit2(child); } if (e.type === "img") { const src = e.attrs.src; if (src) imageInfos.push({ src }); } }, "visit"); for (const child of session.quote.elements) visit2(child); } const textBuffer = []; const resolveBuffer = /* @__PURE__ */ __name(() => { if (!textBuffer.length) return; const bufferTexts = splitArgString(textBuffer.join("")).filter((v) => { if (v === "自己" || v === "@自己") { imageInfos.push({ userId: session.userId }); return false; } if (v.startsWith("@")) { const userId = v.slice(1); imageInfos.push({ userId }); return false; } if (v.startsWith("#")) { const name2 = v.slice(1); names.push(name2); return false; } return true; }); textBuffer.length = 0; texts.push(...bufferTexts); }, "resolveBuffer"); const visit = /* @__PURE__ */ __name((e) => { if (e.children.length) { for (const child of e.children) visit(child); } if (e.type === "text") { const t = e.attrs.content; if (t) textBuffer.push(t); return; } resolveBuffer(); switch (e.type) { case "img": { const src = e.attrs.src; if (src) imageInfos.push({ src }); break; } case "at": { const userId = e.attrs.id; if (userId) imageInfos.push({ userId }); break; } } }, "visit"); for (const child of args) visit(child); resolveBuffer(); return { imageInfos, texts, names }; }; ctx.$.resolveImagesAndInfos = async (session, imageInfos, existingNames) => { const imageInfoKeys = imageInfos.map((v) => JSON.stringify(v)); const imageMap = {}; const userInfoMap = {}; const tasks = [...new Set(imageInfoKeys)].map(async (key) => { const index = imageInfoKeys.indexOf(key); const info = imageInfos[index]; let url; let userInfo; if ("src" in info) { url = info.src; userInfo = {}; } else if ("userId" in info) { ; ({ url, userInfo } = await ctx.$.getInfoFromID(session, info.userId)); } else { throw new Error("Invalid image info"); } imageMap[key] = constructBlobFromFileResp(await ctx.http.file(url)); userInfoMap[key] = userInfo; }); await Promise.all(tasks); const images = imageInfoKeys.map((key) => imageMap[key]); const userInfos = imageInfoKeys.map((key) => userInfoMap[key]); const names = [ ...existingNames ?? [], ...userInfos.slice(existingNames?.length ?? 0).map((x) => x.name ?? session.author.nick ?? session.username) ]; const gender = userInfos.find((x) => x.gender)?.gender ?? "unknown"; return { images, names, gender }; }; ctx.$.checkAndCountToGenerate = async (session) => { ; (session.memesApi ??= {}).inGenerateSubCommand = true; const fatherRet = await session.execute("meme.generate", true); delete session.memesApi.inGenerateSubCommand; return fatherRet.length ? fatherRet : void 0; }; ctx.$.uploadImages = async (images) => { const pLimit = (await import("p-limit")).default; const sem = pLimit(config.requestConcurrency); return Promise.all( images.map( (x) => sem(() => ctx.$.api.uploadImageMultipart(x)).then((x2) => x2.image_id) ) ); }; ctx.$.uploadImagesAndProcess = async (meme, { images, names, gender }, options) => { const imageIds = await ctx.$.uploadImages(images); const imagesReq = imageIds.map((id, k) => ({ id, name: names[k] })); if (options && meme.params.options.some((v) => v.name === "gender") && !("gender" in options)) { options.gender = gender; } return imagesReq; }; ctx.$.normalizeOptionType = (type) => { if (type === "float") return "number"; return type; }; ctx.$.checkOptions = async (session, options, info) => { const { params: { options: memeOpts } } = info; for (const opt of memeOpts) { if (!(opt.name in options)) continue; if (opt.minimum || opt.maximum) { const curr = parseFloat(options[opt.name]); if (isNaN(curr)) { return session.text("memes-api.errors.option-type-mismatch.number", [ opt.name ]); } if (opt.minimum && curr < opt.minimum) { return session.text("memes-api.errors.option-number-too-small", [ opt.name, opt.minimum ]); } if (opt.maximum && curr > opt.maximum) { return session.text("memes-api.errors.option-number-too-big", [ opt.name, opt.maximum ]); } } } }; ctx.$.uploadImgAndRenderMeme = async (meme, texts, uploadInfo, options) => { const imgResp = await ctx.$.api.renderMeme(meme.key, { texts, images: await ctx.$.uploadImagesAndProcess(meme, uploadInfo, options), options }); return await ctx.$.api.getImage(imgResp.image_id); }; const registerGenerateOptions = /* @__PURE__ */ __name((cmd, options) => { for (const opt of options) { const { type, name: name2, description, parser_flags: { short_aliases: sa, long_aliases: la }, choices } = opt; const kType = ctx.$.normalizeOptionType(type); const cfg = { aliases: [...sa, ...la] }; if (choices && choices.length) { cfg.type = choices; } cmd.option(name2, `[${name2}:${kType}] ${description}`, cfg); } return cmd; }, "registerGenerateOptions"); const registerGenerateCmd = /* @__PURE__ */ __name((meme) => { const { key, keywords } = meme; const subCmd = cmdGenerate.subcommand(`.${key} [args:el]`, { strictOptions: true, hidden: true }); for (const kw of keywords) { try { subCmd.alias(`.${kw}`); } catch (e) { ctx.logger.warn(`Failed to register alias ${kw} for meme ${key}`); ctx.logger.warn(e); } } registerGenerateOptions(subCmd, meme.params.options); return subCmd.action(async ({ session, options }, args) => { if (!session || !session.userId) return; if (config.generateSubCommandCountToFather) { const msg = await ctx.$.checkAndCountToGenerate(session); if (msg) return msg; } if (options) { const err = await ctx.$.checkOptions(session, options, meme); if (err) return err; } let resolvedArgs; try { resolvedArgs = await ctx.$.resolveArgs(session, args ?? []); } catch (e) { return ctx.$.handleResolveArgsError(session, e); } const { imageInfos, texts, names } = resolvedArgs; const { params: { min_images: minImages, max_images: maxImages, min_texts: minTexts, max_texts: maxTexts, default_texts: defaultTexts } } = meme; const autoUseAvatar = !!(config.autoUseSenderAvatarWhenOnlyOne && !imageInfos.length && minImages === 1 || config.autoUseSenderAvatarWhenOneLeft && imageInfos.length && imageInfos.length + 1 === minImages); if (autoUseAvatar) { imageInfos.unshift({ userId: session.userId }); } if (!texts.length && config.autoUseDefaultTexts) { texts.push(...defaultTexts); } if (!checkInRange(imageInfos.length, minImages, maxImages)) { return config.silentShortcut && session.memesApi.shortcut ? void 0 : session.text("memes-api.errors.image-number-mismatch", [ formatRange(minImages, maxImages), imageInfos.length ]); } if (!checkInRange(texts.length, minTexts, maxTexts)) { return config.silentShortcut && session.memesApi.shortcut ? void 0 : session.text("memes-api.errors.text-number-mismatch", [ formatRange(minTexts, maxTexts), texts.length ]); } let uploadInfo; try { uploadInfo = await ctx.$.resolveImagesAndInfos(session, imageInfos, names); } catch (e) { return ctx.$.handleResolveImagesAndInfosError(session, e); } let res; try { res = await ctx.$.uploadImgAndRenderMeme(meme, texts, uploadInfo, options); } catch (e) { return ctx.$.handleRenderError(session, e); } return import_koishi2.h.image(await res.arrayBuffer(), res.type); }); }, "registerGenerateCmd"); ctx.$.reRegisterGenerateCommands = async () => { for (const cmd of generateSubCommands) cmd.dispose(); generateSubCommands.length = 0; generateSubCommands.push( ...Object.values(ctx.$.infos).map((v) => registerGenerateCmd(v)) ); }; } __name(apply5, "apply"); // src/commands/info.ts var import_koishi3 = require("koishi"); async function apply6(ctx, config) { const subCmd = ctx.$.cmd.subcommand(".info <query:string>", { checkArgCount: true }); if (config.enableShortcut) { subCmd.alias("表情详情").alias("表情帮助").alias("表情示例"); } subCmd.action(async ({ session }, query) => { if (!session) return; let info; if (query in ctx.$.infos) { info = ctx.$.infos[query]; } else { let searchRes; try { searchRes = await ctx.$.api.searchMemes(query, true); } catch (e) { return ctx.$.handleError(session, e); } if (!searchRes.length) { return session?.text("memes-api.errors.no-such-meme", [query]); } let exactMatch; if (searchRes.length === 1) { exactMatch = ctx.$.infos[searchRes[0]]; } else { const found = searchRes.find((x) => { const info2 = ctx.$.infos[x]; return info2.keywords.includes(query); }); if (found) { exactMatch = ctx.$.infos[found]; } } if (!exactMatch) { let img; try { const keys = await ctx.$.api.getKeys(); const notExistKeys = keys.filter( (x) => !searchRes.includes(x) || !(x in ctx.$.infos) ); if (notExistKeys.length >= keys.length) { return session?.text("memes-api.errors.no-such-meme", [query]); } const imgId = await ctx.$.api.renderList({ meme_properties: {}, exclude_memes: notExistKeys, add_category_icon: config.listAddCategoryIcon, sort_by: config.listSortByRs, sort_reverse: config.listSortReverse, text_template: config.searchListTextTemplate }); img = await ctx.$.api.getImage(imgId.image_id); } catch (e) { return ctx.$.handleError(session, e); } return [ ...session.i18n("memes-api.info.multiple-tip-head"), import_koishi3.h.image(await img.arrayBuffer(), img.type), ...session.i18n("memes-api.info.multiple-tip-tail") ]; } const name2 = searchRes[0]; if (!(name2 in ctx.$.infos)) { return session?.text("memes-api.errors.no-such-meme", [query]); } info = ctx.$.infos[name2]; } const p = info.params; const msg = [ session.i18n("memes-api.info.key", [info.key]), session.i18n("memes-api.info.keywords", [formatKeywords(info.keywords)]) ]; if (info.shortcuts.length) { msg.push( session.i18n("memes-api.info.shortcuts", [ formatKeywords(info.shortcuts.map((v) => v.humanized ?? v.pattern)) ]) ); } if (p.max_images) { msg.push( session.i18n("memes-api.info.image-num", [ formatRange(p.min_images, p.max_images) ]) ); } if (p.max_texts) { msg.push( session.i18n("memes-api.info.text-num", [ formatRange(p.min_texts, p.max_texts) ]), session.i18n("memes-api.info.default-texts", [formatKeywords(p.default_texts)]) ); } if (p.options.length) { const optInfos = p.options.map((v) => { return session.i18n("memes-api.info.option", [ [v.name, ...v.parser_flags.short_aliases, ...v.parser_flags.long_aliases].map((v2) => v2.length > 1 ? `--${v2}` : `-${v2}`).join(session.text("memes-api.info.option-sep")), v.type === "boolean" ? "" : ` [${v.name}: ${v.type}]`, v.description ]); }); msg.push( session.i18n("memes-api.info.options", [ listJoin(optInfos, [import_koishi3.h.text("\n")]).flat() ]) ); } let previewImg; try { const preview = await ctx.$.api.renderPreview(info.key); previewImg = await ctx.$.api.getImage(preview.image_id); } catch (e) { return ctx.$.handleError(session, e); } msg.push( session.i18n("memes-api.info.preview", [ import_koishi3.h.image(await previewImg.arrayBuffer(), previewImg.type) ]) ); return listFlatJoin(msg, ["\n"]); }); } __name(apply6, "apply"); // src/commands/list.ts var import_koishi5 = require("koishi"); // src/config.ts var import_koishi4 = require("koishi"); var import_meme_generator_rs_api2 = require("meme-generator-rs-api"); // src/locales/zh-CN.yml var zh_CN_default = { commands: { "memes-api": { description: "制作各种沙雕表情包" }, "memes-api.list": { description: "查看表情列表" }, "memes-api.info": { description: "查看表情详情(支持模糊搜索及按标签搜索)" }, "memes-api.generate": { description: "生成表情包(每个表情生成指令会注册为本指令的子指令)", usage: "可使用 `自己`,`@自己`,`@某人`,`@用户ID` 格式的参数使用对应用户的头像作为图片参数输入。\n可在使用指令的同时回复消息,被回复消息中的图片将会作为图片参数输入。\n每张图片会附带一个图片名,使用用户头像作为输入时,会将用户昵称作为图片名,否则使用发送者昵称作为图片名。\n可使用且可使用多次 `#名称` 格式的参数覆盖原位置上的图片名。", examples: 'meme generate 5000兆 我去 洛天依\nmeme generate rua -圆 @自己\nmeme generate steam消息 #一大块小饼干 "Visual Studio Code"' }, "memes-api.random": { description: "生成随机表情" } }, "memes-api": { errors: { "syntax-error": { "unexpected-char": "参数语法错误,遇到意外字符 {char} ( 索引 {index} ),如果是左引号请考虑使用反斜杠转义。", "unterminated-quote": "参数语法错误,遇到未闭合引号 {char} ( 索引 {index} )。" }, "can-not-get-avatar": "无法获取平台 {platform} 中 ID 为 {userId} 的用户的头像信息。", "download-image-failed": "下载图片失败。", "option-type-mismatch": { number: "选项 {0} 类型不符,应为数值。" }, "option-number-too-small": "选项 {0} 数值过小,应大于 {1}。", "option-number-too-big": "选项 {0} 数值过大,应小于 {1}。", "no-such-meme": "未找到表情 {0}。", "image-number-mismatch": "输入图片数量不符,图片数量应为 {0},但当前为 {1}。", "text-number-mismatch": "输入文字数量不符,文字数量应为 {0},但当前为 {1}。", "image-decode": "图片解码失败:{0}。", "image-encode": "图片编码失败:{0}。", "image-asset-missing": "缺少图片资源:{0}。", deserialize: "表情选项解析出错:{0}。", "text-over-length": "输入文字过长。", "meme-feedback": "{0}。", "other-error": "发生其他错误:({0}) {1}。" }, list: { tip: "触发方式:“关键词 + 图片/文字”\n发送 “表情详情 + 关键词” 查看表情参数和预览\n目前支持的表情列表:\n{0}", "tip-no-shortcut": "触发指令:“meme generate <关键词/序号> [...图片/文字]”\n发送指令 “meme info <关键词/序号>” 查看表情参数和预览\n目前支持的表情列表:\n{0}" }, info: { "multiple-tip-head": "搜索到多个可能结果:", "multiple-tip-tail": "如有您想要的结果,请修改参数重新查询。", key: "表情名:{0}", keywords: "关键词:{0}", shortcuts: "快捷指令:{0}", "image-num": "需要图片数目:{0}", "text-num": "需要文字数目:{0}", "default-texts": "默认文字:{0}", option: " * {0}{1} - {2}", "option-sep": " | ", options: "可选参数:\n{0}", preview: "表情预览:\n{0}" }, random: { "no-suitable-meme": "找不到符合当前已提供参数信息的表情。", info: "关键词:{0}" } }, _config: [[[[{ $desc: "生成指令配置", enableShortcut: "是否注册类似原版 `memes` 插件的触发指令。 \n例:`meme generate 5000兆 我去 洛天依` -> `5000兆 我去 洛天依`" }, { shortcutUsePrefix: "表情生成快捷指令是否需要携带指令前缀。", silentShortcut: "是否禁用使用原版触发指令时的 参数错误提示。" }], { moreSilent: "是否禁用使用原版触发指令时的 **所有** 错误提示。" }], { autoUseDefaultTexts: "是否在用户未提供文字时自动使用默认文字。", autoUseSenderAvatarWhenOnlyOne: "是否在仅需求一张图片且用户未提供时自动使用发送者头像。", autoUseSenderAvatarWhenOneLeft: "是否在用户已提供图片但距离最少需求图片数仅差 1 时自动使用发送者头像。" }], { $desc: "表情列表配置", listSortByRs: { $desc: "表情排序方式。", $inner: ["表情名", "关键词", "关键词拼音", "创建时间", "修改时间"] }, listSortReverse: "是否倒序排列。", listNewTimeDelta: "表情添加时间在该时间间隔(单位为天)以内时,在列表中添加新表情标识。", listNewStrategy: { $desc: "新表情标识的显示策略。", $inner: ["创建时间", "修改时间"] }, listTextTemplate: "表情列表显示文字模板。 \n可用变量:`{index}`(序号)、`{key}`(表情名)、`{keywords}`(关键词)、`{shortcuts}`(快捷指令)、`{tags}`(标签)", searchListTextTemplate: "表情信息模糊搜索出现多个结果时显示的文字模板。可用变量同上。", listAddCategoryIcon: "是否添加图标以表示表情类型,即“图片表情包”和“文字表情包”。" }, { $desc: "其他指令配置", randomMemeShowInfo: "使用 `meme random` 指令时是否同时发出表情关键词。", generateSubCommandCountToFather: "执行生成表情子指令时是否同时增加父指令的调用计数(对于 `rate-limit` 插件)。", randomCommandCountToGenerate: "执行 `meme random` 指令时是否同时增加 `meme generate` 指令的调用计数(对于 `rate-limit` 插件)。" }, { $desc: "请求配置", requestConfig: { timeout: "等待连接建立的最长时间。", proxyAgent: "使用的代理服务器地址。", keepAlive: "是否保持连接。", endpoint: "要连接的服务器地址。", headers: "要附加的额外请求头。" }, requestConcurrency: "请求并发数。" }] }; // src/config.ts var NewStrategy = /* @__PURE__ */ ((NewStrategy2) => { NewStrategy2["DateCreated"] = "date_created"; NewStrategy2["DateModified"] = "date_modified"; return NewStrategy2; })(NewStrategy || {}); var shortcutCmdConfig = import_koishi4.Schema.object({ enableShortcut: import_koishi4.Schema.boolean().default(true) }); var shortcutCmdCfgWithSilent = import_koishi4.Schema.intersect([ shortcutCmdConfig, import_koishi4.Schema.union([ import_koishi4.Schema.object({ enableShortcut: import_koishi4.Schema.const(true), shortcutUsePrefix: import_koishi4.Schema.boolean().default(true), silentShortcut: import_koishi4.Schema.boolean().default(false) }), import_koishi4.Schema.object({}) ]) ]); var shortcutCmdCfgWithMoreSilent = import_koishi4.Schema.intersect([ shortcutCmdCfgWithSilent, import_koishi4.Schema.union([ import_koishi4.Schema.object({ enableShortcut: import_koishi4.Schema.const(true), silentShortcut: import_koishi4.Schema.const(true).required(), moreSilent: import_koishi4.Schema.boolean().default(false) }), import_koishi4.Schema.object({}) ]) ]); var GenerateCommandConfig = import_koishi4.Schema.intersect([ shortcutCmdCfgWithMoreSilent, import_koishi4.Schema.object({ autoUseDefaultTexts: import_koishi4.Schema.boolean().default(true), autoUseSenderAvatarWhenOnlyOne: import_koishi4.Schema.boolean().default(true), autoUseSenderAvatarWhenOneLeft: import_koishi4.Schema.boolean().default(true) }) ]); var ListConfig = import_koishi4.Schema.object({ listSortByRs: import_koishi4.Schema.union(import_meme_generator_rs_api2.memeListSortByVals).default("keywords_pinyin"), listSortReverse: import_koishi4.Schema.boolean().default(false), listNewTimeDelta: import_koishi4.Schema.natural().min(1).default(30), listNewStrategy: import_koishi4.Schema.union(Object.values(NewStrategy)).default( "date_created" /* DateCreated */ ), listTextTemplate: import_koishi4.Schema.string().default("{keywords}"), searchListTextTemplate: import_koishi4.Schema.string().default("{key}({keywords})"), listAddCategoryIcon: import_koishi4.Schema.boolean().default(true) }); var OtherCommandConfig = import_koishi4.Schema.object({ randomMemeShowInfo: import_koishi4.Schema.boolean().default(true), generateSubCommandCountToFather: import_koishi4.Schema.boolean().default(false), randomCommandCountToGenerate: import_koishi4.Schema.boolean().default(false) }); var RequestConfig = import_koishi4.Schema.object({ requestConfig: import_koishi4.HTTP.createConfig("http://127.0.0.1:2233"), requestConcurrency: import_koishi4.Schema.natural().min(1).default(8) }); var Config = import_koishi4.Schema.intersect([ GenerateCommandConfig, ListConfig, OtherCommandConfig, RequestConfig ]).i18n({ "zh-CN": zh_CN_default._config, zh: zh_CN_default._config }); // src/commands/list.ts async function apply7(ctx, config) { const subCmd = ctx.$.cmd.subcommand(".list"); if (config.enableShortcut) { subCmd.alias("表情包制作").alias("表情列表").alias("头像表情包").alias("文字表情包"); } subCmd.action(async ({ session }) => { if (!session) return; const nowTimestamp = (/* @__PURE__ */ new Date()).getTime(); const timeDeltaMs = config.listNewTimeDelta * import_koishi5.Time.day; const properties = {}; for (const [key, info] of Object.entries(ctx.$.infos)) { const compareTimeStr = config.listNewStrategy === "date_created" /* DateCreated */ ? info.date_created : info.date_modified; const compareTimestamp = new Date(compareTimeStr).getTime(); if (nowTimestamp - compareTimestamp <= timeDeltaMs) { ; (properties[key] ??= {}).new = true; } } let imgBlob; try { const keys = await ctx.$.api.getKeys(); const notExistKeys = keys.filter((x) => !(x in ctx.$.infos)); const img = await ctx.$.api.renderList({ exclude_memes: notExistKeys, meme_properties: properties, sort_by: config.listSortByRs, sort_reverse: config.listSortReverse, text_template: config.listTextTemplate, add_category_icon: config.listAddCategoryIcon }); imgBlob = await ctx.$.api.getImage(img.image_id); } catch (e) { return ctx.$.handleError(session, e); } const msgParams = [import_koishi5.h.image(await imgBlob.arrayBuffer(), imgBlob.type)]; return config.enableShortcut ? session.i18n("memes-api.list.tip", msgParams) : session.i18n("memes-api.list.tip-no-shortcut", msgParams); }); } __name(apply7, "apply"); // src/commands/random.ts var import_koishi6 = require("koishi"); async function apply8(ctx, config) { const subCmd = ctx.$.cmd.subcommand(".random [args:el]"); if (config.enableShortcut) { subCmd.alias("随机表情"); } subCmd.action(async ({ session, options }, args) => { if (!session || !session.userId) return; if (config.randomCommandCountToGenerate) { const msg = await ctx.$.checkAndCountToGenerate(session); if (msg) return msg; } let resolvedArgs; try { resolvedArgs = await ctx.$.resolveArgs(session, args ?? []); } catch (e) { return ctx.$.handleResolveArgsError(session, e); } const { imageInfos, texts, names } = resolvedArgs; const autoUse = !imageInfos.length && !texts.length; if (autoUse) imageInfos.push({ userId: session.userId }); const checkOptionExists = /* @__PURE__ */ __name((info) => { if (!options) return true; const { params: { options: memeOpts } } = info; return !Object.keys(options).some( (k) => ( // 在没找着时返回 true !memeOpts.some((x) => x.name === k) ) ); }, "checkOptionExists"); const castOpt = /* @__PURE__ */ __name((opt) => { if (!options || !(opt.name in options)) return void 0; const { type } = opt; const raw = options[opt.name]; switch (type) { case "boolean": return ["true", "1"].includes(`${raw}`.toLowerCase()); case "integer": return parseInt(raw, 10); case "float": return parseFloat(raw); default: return `${raw}`; } }, "castOpt"); const castOptions = /* @__PURE__ */ __name((info) => { const newOpts = {}; if (!options) return {}; for (const k in options) { const opt = info.params.options.find((x) => x.name === k); if (opt) { newOpts[k] = castOpt(opt); } } return newOpts; }, "castOptions"); const suitableMemes = Object.values(ctx.$.infos).filter((info) => { const { params: { min_images: minImages, max_images: maxImages, min_texts: minTexts, max_texts: maxTexts } } = info; return checkInRange(imageInfos.length, minImages, maxImages) && (autoUse || checkInRange(texts.length, minTexts, maxTexts)) && checkOptionExists(info); }); if (!suitableMemes.length) { return session.text("memes-api.random.no-suitable-meme"); } let uploadInfo; try { uploadInfo = await ctx.$.resolveImagesAndInfos(session, imageInfos, names); } catch (e) { return ctx.$.handleResolveImagesAndInfosError(session, e); } while (suitableMemes.length) { const index = import_koishi6.Random.int(0, suitableMemes.length); const info = suitableMemes[index]; suitableMemes.splice(index, 1); let res; try { const opts = castOptions(info); res = await ctx.$.uploadImgAndRenderMeme(info, texts, uploadInfo, opts); } catch (e) { ctx.logger.warn(e); continue; } const el = [import_koishi6.h.image(await res.arrayBuffer(), res.type)]; if (config.randomMemeShowInfo) { el.unshift( ...session.i18n("memes-api.random.info", [formatKeywords(info.keywords)]) ); } return el; } return session.text("memes-api.random.no-suitable-meme"); }); } __name(apply8, "apply"); // src/commands/shortcut.ts var import_koishi7 = require("koishi"); async function apply9(ctx, config) { if (!config.enableShortcut) return; const shortcuts = []; ctx.$.refreshShortcuts = async () => { const tmpKeywords = []; const tmpRegExps = []; for (const name2 in ctx.$.infos) { const info = ctx.$.infos[name2]; info.keywords.forEach((keyword) => { tmpKeywords.push({ name: name2, keyword }); }); info.shortcuts.forEach((s) => { tmpRegExps.push({ name: name2, ...s, pattern: transformRegex(s.pattern) }); }); } const tmpShortcuts = [ ...tmpKeywords.sort((a, b) => b.keyword.length - a.keyword.length).map(({ name: name2, keyword }) => { return { name: name2, pattern: (0, import_koishi7.escapeRegExp)(keyword) }; }), ...tmpRegExps ]; shortcuts.length = 0; shortcuts.push(...tmpShortcuts); }; const resolveArgs = /* @__PURE__ */ __name((shortcut, res) => { const args = []; if (shortcut.names) { args.push(...shortcut.names?.map((x) => `#${replaceBracketVar(x, res)}`)); } if (shortcut.texts) { args.push(...shortcut.texts?.map((x) => replaceBracketVar(x, res))); } const options = shortcut.options ? [ ...Object.entries(shortcut.options).map( ([key, value]) => `${key.length > 1 ? "--" : "-"}${key} ${escapeArgs([replaceBracketVar(value, res)])}` ) ].join(" ") : ""; return [options, args.join(" ")].filter(Boolean).join(" "); }, "resolveArgs"); ctx.middleware(async (session, next) => { const { content } = session; if (!content) return next(); const cmdPrefixRegex = (() => { if (config.shortcutUsePrefix) { const cmdPfxCfg = session.resolve(ctx.root.config.prefix); const cmdPfx = cmdPfxCfg instanceof Array ? cmdPfxCfg : [cmdPfxCfg ?? ""]; const hasEmptyPfx = cmdPfx.includes(""); const cmdPfxNotEmpty = cmdPfx.filter(Boolean); if (cmdPfxNotEmpty.length) { return `(?:${cmdPfxNotEmpty.map(import_koishi7.escapeRegExp).join("|")})${hasEmptyPfx ? "?" : ""}`; } } return ""; })(); for (const shortcut of shortcuts) { const { name: name2, pattern } = shortcut; const res = new RegExp(`^${cmdPrefixRegex}${pattern}`).exec(content); if (res) { ; (session.memesApi ??= {}).shortcut = true; return session.execute( `meme.generate.${name2} ${resolveArgs(shortcut, res)} ${content.slice(res.index + res[0].length)}` ); } } return next(); }); } __name(apply9, "apply"); // src/commands/index.ts async function apply10(ctx, config) { ctx.$.cmd = ctx.command("memes-api").alias("memes").alias("meme"); await apply5(ctx, config); await apply9(ctx, config); await apply8(ctx, config); await apply7(ctx, config); await apply6(ctx, config); } __name(apply10, "apply"); // src/index.tsx var import_jsx_runtime = require("@satorijs/element/jsx-runtime"); var name = "memes-api"; var usage = ` <style> .memes-api-usage { background-color: var(--k-side-bg); padding: 1px 24px; border-radius: 4px; border-left: 4px solid var(--k-color-primary); } .memes-api-usage a { color: var(--k-color-primary-tint); } .memes-api-usage a:hover { color: var(--primary); } .k-comment.success > ul > li:has(a[href^='/commands/memes-api/generate/']) { display: none; } .k-comment.success > ul > li:has(a[href^='/commands/memes-api/generate'])::after { content: '(子命令已隐藏)'; font-size: 12px; color: var(--k-text-normal); } </style> <div class="memes-api-usage"> 好消息,memes-api v2 已经初步支持 [meme-generator-rs](https://github.com/MemeCrafters/meme-generator-rs) 🎉 v2 版本将仅支持 meme-generator-rs,如要使用旧版 meme-generator,请回退到 v1 版本。 查看 [部署文档](https://github.com/MemeCrafters/meme-generator-rs/wiki/%E6%9C%AC%E5%9C%B0%E5%AE%89%E8%A3%85) 或者看看我做的 [视频教程](https://www.bilibili.com/video/BV1hBVsz6EvV/ '关注 LgCookie 喵,关注 LgCookie 谢谢喵') 了解如何部署后端。 目前插件还是处于 可能可以正常使用 的状态, 如果有 Bug 请积极 [反馈](https://github.com/lgc-KoiDev/koishi-plugin-memes-api/issues), [这里](https://github.com/lgc-KoiDev/koishi-plugin-memes-api#-%E9%85%8D%E7%BD%AE--%E4%BD%BF%E7%94%A8) 也有一些暂缓实现的功能,如果真的很想要可以催催我! 感谢各位的支持与使用~~~!🤗❤️ </div> `.trim(); var inject = { required: ["http"], optional: ["notifier"] }; async function apply11(ctx, config) { ctx.i18n.define("zh-CN", zh_CN_default); ctx.i18n.define("zh", zh_CN_default); ctx = ctx.isolate("$"); ctx.set("$", {}); ctx.$.api = new import_meme_generator_rs_api3.MemeAPI(ctx.http.extend(config.requestConfig)); await apply4(ctx, config); ctx.inject(["notifier"], () => { ctx.$.notifier = ctx.notifier.create(); }); ctx.$.notifier?.update({ type: "primary", content: "插件初始化中……" }); try { await ctx.$.updateInfos(); } catch (e) { ctx.logger.warn("Failed to fetch data from backend, plugin will not work"); ctx.logger.warn(e); const is404 = e instanceof import_meme_generator_rs_api3.MemeError && e.httpStatus === 404; ctx.$.notifier?.update({ type: "danger", content: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [ "从后端获取相关信息失败,插件将不会工作!", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}), is404 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: "你或许还没有在使用 meme-generator-rs?请参考插件介绍迁移到 meme-generator-rs 哦。更多信息请查看日志。" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: "请检查你的请求设置以及 meme-generator 的部署状态,更多信息请查看日志。" }) ] }) }); return; } try { await apply10(ctx, config); await ctx.$.reRegisterGenerateCommands(); await ctx.$.refreshShortcuts?.(); } catch (e) { try { ctx.$.cmd?.dispose(); } catch (_) { } ctx.logger.warn("Failed to initialize, plugin will not work"); ctx.logger.warn(e); ctx.$.notifier?.update({ type: "danger", content: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [ "注册插件指令时出错,插件将不会工作!", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}), "更多信息请查看日志。" ] }) }); return; } ctx.$.$public = { get api() { return ctx.$.api; }, get infos() { return ctx.$.infos; }, get apiVersion() { return ctx.$.apiVersion; } }; ctx.set("memesApi", ctx.$.$public); const memeCount = Object.keys(ctx.$.infos).length; const minVersion = [0, 2, 2]; const versionMeets = isVersionMeets(ctx.$.apiVersion, minVersion); ctx.$.notifier?.update({ type: versionMeets ? "success" : "warning", content: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [ versionMeets ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ "警告:后端版本需大于等于 ", minVersion.join("."), ",否则插件可能无法正常使用!", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}) ] }), "插件初始化完毕,后端版本 ", ctx.$.apiVersion, ",共载入 ", memeCount, " 个表情。" ] }) }); ctx.logger.info( `Plugin initialized successfully, backend version ${ctx.$.apiVersion}, loaded ${memeCount} memes` ); } __name(apply11, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, inject, name, usage });