UNPKG

koishi-plugin-emojihub-bili

Version:

[<ruby>Emojihub-bili<rp>(</rp><rt>点我查看食用方法</rt><rp>)</rp></ruby>](https://www.npmjs.com/package/koishi-plugin-emojihub-bili)🐱支持QQ官方json按钮!20群即可发按钮!😍可自定义添加或删除任意表情包!基本每个指令都有上千张!支持定时/定量消息触发![点我查看食用方法](https://www.npmjs.com/package/koishi-plugin-emojihub-bi

421 lines (381 loc) 19 kB
import { Context, h, Session, Universal } from "koishi"; import { Config, usage } from "./config"; import { logInfo, logError, logInfoformat, replacePlaceholders } from "./utils"; import { determineImagePath, getRandomEmojiHubCommand, listAllCommands, } from "./core"; import { markdown, command_list_markdown, sendmarkdownMessage } from "./markdown"; import { loadCanvasImageSource } from "./canvas-source"; import { registerLocalImageProxyRoute } from "./media-proxy"; import { } from '@koishijs/assets'; import { } from '@koishijs/plugin-server'; import { } from "koishi-plugin-cron"; import { } from "koishi-plugin-canvas"; export const inject = { required: ['server'], optional: ['canvas', 'assets'] }; export const name = 'emojihub-bili'; export const reusable = true; export { Config, usage }; export function apply(ctx: Context, config: Config) { const emojihub_bili_codecommand = config.emojihub_bili_command; registerLocalImageProxyRoute(ctx); async function getLocalImageSize(canvasSource: string) { if (!ctx.canvas) throw new Error('canvas service not available'); logInfo(config, `获取本地图片尺寸,canvasSource: ${canvasSource}`); const canvasimage = await loadCanvasImageSource(ctx, canvasSource); // @ts-ignore const width = canvasimage.naturalWidth || canvasimage.width; // @ts-ignore const height = canvasimage.naturalHeight || canvasimage.height; return { width, height }; } async function resolveAssetsUrlForMarkdown(imageUrl: string, imagePath?: string) { if (!ctx.assets) throw new Error('assets service not available'); const localTarget = imageUrl || imagePath; const transformed = await ctx.assets.transform(String(h.image(localTarget))); const match = transformed.match(/<img\s+src="([^"]+)"/i); if (!match?.[1]) throw new Error(`assets.transform did not return an image url: ${transformed}`); return h.unescape(match[1]); } ctx.i18n.define("zh-CN", { commands: { [emojihub_bili_codecommand]: { description: `表情包功能`, messages: { "notfound_txt": "ERROR!找不到文件或文件为空!指令:{0}", "List_of_emojis": "可用的表情包指令:{0}", "notallowednum": `{0}次超出单次返回最大值\n请使用指令:{1} -n {2}`, } }, [config.emojihub_onemore]: { description: `触发上次的表情包`, messages: { "nocommand": `没有找到上一个命令,请先执行一个命令!\n➣例如: ${config.emojihub_randompic}`, } }, [config.emojihub_randompic]: { description: `从全部表情包里随机抽`, messages: { "noemoji": "没有任何表情包配置,请检查插件配置项!", } } } } ); const lastCommandByChannel = {}; function updateLastCommand(differentiationID: string, command: string) { lastCommandByChannel[differentiationID] = command; logInfo(config, '记录到command为: ' + command + ' 区别ID: ' + differentiationID); } async function sendMultipleEmojis(session: Session, command: string, num: number) { const maxAllowed = config.maxexecutetime || 10; // 使用配置中的最大数量,默认为10 if (num > maxAllowed) { await session.send(h.text(session.text(`commands.${emojihub_bili_codecommand}.messages.notallowednum`, [num, command, maxAllowed]))); return; // 不继续执行 } const numToSend = Math.min(num || 1, maxAllowed); // 确定要发送的数量,不超过最大值 for (let i = 0; i < numToSend; i++) { // 如果是“再来一张”指令,则需要特殊处理 if (command === config.emojihub_onemore) { const identifier = config.repeatCommandDifferentiation === 'userId' ? session.userId : session.channelId; const lastCommand = lastCommandByChannel[identifier]; if (lastCommand) { await session.execute(lastCommand); } else { await session.send(session.text(".nocommand")); return; // 如果没有上一个命令,则直接返回,不再继续循环 } } else { // 对于其他指令,直接执行 await session.execute(command); } } } ctx.command(config.emojihub_bili_command) .action(async ({ session }) => { const txtCommandList = listAllCommands(config); logInfo(config, `指令列表txtCommandList: ` + txtCommandList); if (session.platform === "qq" || session.platform === "qqguild") { let markdownMessage = command_list_markdown(ctx, session, config, emojihub_bili_codecommand, 1); await sendmarkdownMessage(ctx, session, markdownMessage, config); } else { const commandText = txtCommandList.join('\n'); await session.send(h.text(session.text(`commands.${emojihub_bili_codecommand}.messages.List_of_emojis`, [`\n${commandText}`]))); } }); ctx.on('ready', () => { config.MoreEmojiHubList.forEach(({ command, source_url }) => { ctx.command(`${config.emojihub_bili_command}/${command} [local_picture_name...]`) .example(`${command} 关键词1 关键词2 关键词3`) .option('numpics', `-n <numpics:number> 指定返回数量`) .action(async ({ session, options }, ...local_picture_name) => { if (options?.numpics) { await sendMultipleEmojis(session, `${command} ${local_picture_name.join(' ')}`.trim(), options.numpics); return; } const imageResult = await determineImagePath(source_url, config, session.channelId, command, ctx, local_picture_name); if (!imageResult.imageUrl) { await session.send(h.text(session.text(`commands.${emojihub_bili_codecommand}.messages.notfound_txt`, [command]))); return; } // 根据 config.repeatCommandDifferentiation 的值选择合适的 ID const identifier = config.repeatCommandDifferentiation === 'userId' ? session.userId : session.channelId; updateLastCommand(identifier, command); try { let message; if ((session.platform === "qq" || session.platform === "qqguild") && (config.markdown_button_mode === "raw")) { if (imageResult.isLocal) { const imageSize = imageResult.imageUrl ? await getLocalImageSize(imageResult.imageUrl) : undefined; const assetsUrl = await resolveAssetsUrlForMarkdown(imageResult.imageUrl, imageResult.imagePath); logInfo(config, `[assets] converted local image to url: ${assetsUrl}`); message = await markdown(ctx, session, command, assetsUrl, config, imageResult.imageUrl, imageSize); await sendmarkdownMessage(ctx, session, message, config); } else { message = await markdown(ctx, session, command, imageResult.imageUrl, config); await sendmarkdownMessage(ctx, session, message, config); } } else { if (imageResult.isLocal) { const format = config.localPictureToName; logInfo(config, imageResult.imageUrl) // 格式化文件大小 const fileSizeKB = (imageResult.imageSize / 1024).toFixed(2); const fileSizeMB = (imageResult.imageSize / (1024 * 1024)).toFixed(2); const formattedSize = imageResult.imageSize < 1024 * 1024 ? `${fileSizeKB} KB` : `${fileSizeMB} MB`; // 格式化时间 const formattedTime = imageResult.imageTime.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-'); const imageBuffer = Buffer.from((await ctx.http.file(imageResult.imageUrl)).data); const base64Data = imageBuffer.toString('base64'); const assetsUrl = `data:image/png;base64,${base64Data}`; const context = { IMAGE: h.image(assetsUrl), NAME: imageResult.imageName, TIME: formattedTime, SIZE: formattedSize, PATH: imageResult.imagePath, }; const messageContent = replacePlaceholders(format, context); logInfo(config, "变量替换本地文件名称,messageContent:", messageContent) try { message = await session.send(h.unescape(`${messageContent}`.replace(/\\n/g, '\n'))); } catch (error) { ctx.logger.error("发送本地图片失败:", error) } } else { // 网络图片 try { message = await session.send(h.image(imageResult.imageUrl)); } catch (error) { ctx.logger.error("发送网络图片失败:", error) } } } if (config.deleteMsg) { ctx.setTimeout(async () => { try { await session.bot.deleteMessage(session.channelId, message); } catch (error) { logError(`撤回消息失败: ${error}`); logError(error); } }, config.deleteMsgtime * 1000); } } catch (error) { logError(`Error sending image: ${error}`); logError(error) } }); }) }); ctx.command(`${config.emojihub_bili_command}/${config.emojihub_onemore}`) .action(async ({ session, options }) => { const identifier = config.repeatCommandDifferentiation === 'userId' ? session.userId : session.channelId; const lastCommand = lastCommandByChannel[identifier]; logInfo(config, '尝试在区分ID ' + identifier + ' 中执行最后一个命令: ' + lastCommand); if (lastCommand) { await session.execute(`${lastCommand}`); } else { await session.send(session.text(".nocommand")); } }); ctx.command(`${config.emojihub_bili_command}/${config.emojihub_randompic}`) .action(async ({ session, options }) => { const randomEmojiHubCommand = getRandomEmojiHubCommand(config); if (randomEmojiHubCommand) { await session.execute(randomEmojiHubCommand); logInfoformat(config, session.channelId, randomEmojiHubCommand, config.emojihub_randompic); return; } else { await session.send(session.text(".noemoji")); } }); if (config.autoEmoji === "定量消息发送" && (config.groupListmapping.length || config.allgroupautoEmoji)) { const groups = {}; // 初始化特定群组的配置 config.groupListmapping.forEach(({ groupList, defaultemojicommand, count, enable }) => { if (enable === true) { // 如果enable为true,则将该群组标记为黑名单 groups[groupList] = { blacklisted: true }; } else { groups[groupList] = { emojicommand: defaultemojicommand, threshold: count }; } }); ctx.middleware(async (session, next) => { const channelId = session.channelId; // 确定当前群组是否在特定配置中并且是否被黑名单 let groupConfig = groups[channelId]; // 如果当前群组标记为黑名单,则直接跳过处理 if (groupConfig && groupConfig.blacklisted) { return next(); } // 如果当前群组没有特定配置,并且开启了全部群组自动表情包 if (!groupConfig && config.allgroupautoEmoji) { // 初始化为全部群组的配置 groupConfig = { count: 0, emojicommand: config.allgroupemojicommand, threshold: config.count }; groups[channelId] = groupConfig; // 记录配置以供后续使用 } // 如果存在配置,处理表情包逻辑 if (groupConfig) { groupConfig.count = (groupConfig.count || 0) + 1; // 增加消息计数 logInfo(config, `${channelId}${groupConfig.count}${session.content}`) // 达到触发条件 if (groupConfig.count >= groupConfig.threshold) { const randomNumber = Math.random(); // 触发概率判断 if (randomNumber <= config.triggerprobability) { logInfo(config, `定量消息发送:概率判断:${randomNumber} <= ${config.triggerprobability} 触发表情包`) // 打印触发日志 let emojicommands = groupConfig.emojicommand.split(/\n|,|,/).map(cmd => cmd.trim()); const randomCommand = emojicommands[Math.floor(Math.random() * emojicommands.length)]; logInfo(config, `随机选择的指令: ${randomCommand}`); const emojiConfig = config.MoreEmojiHubList.find(({ command }) => command === randomCommand); if (emojiConfig) { const imageResult = await determineImagePath(emojiConfig.source_url, config, channelId, emojiConfig.command, ctx); if (imageResult.imageUrl) { try { groupConfig.count = 0; // 重置消息计数 let message; if (imageResult.isLocal) { //本地图片 message = h.image(imageResult.imageUrl); } else { message = h.image(imageResult.imageUrl); } let sentMessage = await session.send(message); // 如果需要撤回消息 if (config.deleteMsg) { ctx.setTimeout(async () => { try { await session.bot.deleteMessage(session.channelId, sentMessage[0]); } catch (error) { logError(`撤回消息失败: ${error}`); } }, config.deleteMsgtime * 1000); } } catch (error) { logError(`发送图片错误: ${error}`); } } else { groupConfig.count = 0; // 图片不存在,重置计数 } } } else { groupConfig.count = 0; // 没有触发表情包,重置计数 const comparisonSymbol = randomNumber <= config.triggerprobability ? "<=" : ">"; // 根据比较结果设置比较符号 logInfo(config, `定量消息发送:概率判断:${randomNumber} ${comparisonSymbol} ${config.triggerprobability}\n此次不发送表情包,并且重置计数。`) } } } return next(); }, config.middleware); } ctx.on('ready', () => { if (config.autoEmoji === "定时发送" && config.groupListmapping.length && ctx.cron) { // const bot = ctx.bots[config.bot]; const bot = Object.values(ctx.bots).find(b => b.selfId === config.botId || b.user?.id === config.botId); if (!bot || bot.status !== Universal.Status.ONLINE) { ctx.logger.error(`[定时发送] 机器人离线或未找到: ${config.botId}`); return; } else { ctx.logger.info(`定时成功:将由 ${config.botId} 执行`); } if (bot == null) return; const groups = {}; // 初始化特定群组的配置 config.groupListmapping.forEach(({ groupList, defaultemojicommand, cronTime, enable }) => { if (enable === true) { // 如果enable为true,则将该群组标记为黑名单 groups[groupList] = { blacklisted: true }; } else { groups[groupList] = { emojicommand: defaultemojicommand, cronTime }; } }); // 定时触发表情包 for (const channelId in groups) { const groupConfig = groups[channelId]; // 如果当前群组标记为黑名单,则跳过处理 if (groupConfig && groupConfig.blacklisted) { continue; } // 如果当前群组没有特定配置,则跳过 if (!groupConfig) { continue; } // 如果存在配置,设置定时任务 if (groupConfig) { ctx.inject(['cron'], (ctx) => { ctx.cron(groupConfig.cronTime, async () => { const randomNumber = Math.random(); // 触发概率判断 if (randomNumber <= config.triggerprobability) { logInfo(config, `尝试向 ${channelId} 定时发送表情包中...`) let emojicommands = groupConfig.emojicommand.split(/\n|,|,/).map(cmd => cmd.trim()); const randomCommand = emojicommands[Math.floor(Math.random() * emojicommands.length)]; const emojiConfig = config.MoreEmojiHubList.find(({ command }) => command === randomCommand); if (emojiConfig) { const imageResult = await determineImagePath(emojiConfig.source_url, config, channelId, emojiConfig.command, ctx); if (imageResult.imageUrl) { try { let message; if (imageResult.isLocal) { //本地图片 message = h.image(imageResult.imageUrl); } else { message = h.image(imageResult.imageUrl); } // 判断是群聊还是私聊 if (!channelId.includes("private")) { await bot.sendMessage(channelId, message); } else { const userId = channelId.replace("private:", ""); await bot.sendPrivateMessage(userId, message); } // 如果需要撤回消息 if (config.deleteMsg) { ctx.setTimeout(async () => { try { await bot.deleteMessage(channelId, message); } catch (error) { logError(`撤回消息失败: ${error}`); } }, config.deleteMsgtime * 1000); } } catch (error) { logError(`发送图片错误: ${error}`); } } } } else { const comparisonSymbol = randomNumber <= config.triggerprobability ? "<=" : ">"; // 根据比较结果设置比较符号 logInfo(config, `定时发送:概率判断结果:${randomNumber} ${comparisonSymbol} ${config.triggerprobability}\n此次不发送表情包。`) } }); }) } } } else if (config.autoEmoji === "定时发送" && config.groupListmapping.length && !ctx.cron) { ctx.logger.error("cron 服务加载失败!") } }) }