UNPKG

@oebot/core

Version:

能跑就行的 QQ 机器人框架,基于 oicq v2,改自KiviBot(R.I.P.)

460 lines (459 loc) 18.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OEPlugin = void 0; const fs_extra_1 = __importDefault(require("fs-extra")); const log4js_1 = __importDefault(require("log4js")); const minimist_1 = __importDefault(require("minimist")); const node_cron_1 = __importDefault(require("node-cron")); const node_events_1 = __importDefault(require("node:events")); const node_path_1 = __importDefault(require("node:path")); const pluginError_1 = require("./pluginError"); const events_1 = require("../events"); const src_1 = require("../../index"); const utils_1 = require("../../utils"); class OEPlugin extends node_events_1.default { /** * OEBot 插件类 * * @param {string} name 插件名称,建议英文,插件数据目录以此结尾 * @param {string} version 插件版本,如 1.0.0,建议 require `package.json` 的版本号统一管理 * @param conf */ constructor(name, version, conf) { super(); /** 向框架输出日志记录器,是 log4js 的实例 */ this.logger = log4js_1.default.getLogger('plugin'); /** 挂载的 Bot 实例 */ this.bot = null; /** 插件配置 */ this.config = {}; this._cronTasks = []; this._handlers = new Map(); this._dataDir = node_path_1.default.join(src_1.PluginDataDir, 'null'); this._mounted = () => { }; this._unmounted = () => { }; this.name = name ?? 'null'; this.version = version ?? 'null'; this._dataDir = node_path_1.default.join(src_1.PluginDataDir, this.name); this.config = conf ?? {}; this.debug('create OEPlugin instance'); } /** * 框架管理员列表 (getter),插件会自动监听变动事件,并保证列表是实时最新的 */ get admins() { this.checkMountStatus(); return [...(this._admins || [])]; } /** * 插件数据存放目录,`框架目录/data/plugins/<name>` 注意这里的 name 是实例化的时候传入的 name */ get dataDir() { if (!fs_extra_1.default.existsSync(this._dataDir)) { fs_extra_1.default.ensureDirSync(this._dataDir); } return this._dataDir; } /** * 框架主管理员 (getter),插件会自动监听变动事件,并保证列表是实时最新的 */ get mainAdmin() { this.checkMountStatus(); return [...(this._admins || [])][0]; } /** * 框架副管理员列表 (getter),插件会自动监听变动事件,并保证列表是实时最新的 */ get subAdmins() { this.checkMountStatus(); return (this._admins || []).slice(1); } /** * 抛出一个 OEBot 插件标准错误,会被框架捕获并输出到日志 * * @param {string} message 错误信息 */ throwPluginError(message) { throw new pluginError_1.OEPluginError(this.name, message); } /** * **插件请勿调用**,OEBot 框架调用此函数启用插件 * @param {Client} bot oicq Client 实例 * @param {AdminArray} admins 框架管理员列表 * @return {Promise<OEPlugin>} 插件实例 Promise */ async mountOEBotClient(bot, admins) { this.debug('mountOEBotClient'); // 挂载 Bot this.bot = bot; // 初始化管理员 this._admins = [...admins]; // 监听框架管理变动 bot.on('oe.admins', this.adminChangeHandler); try { this.debug('_mounted'); // 调用 onMounted 挂载的函数 const res = this._mounted(bot, [...this._admins]); // 如果是 Promise 等待其执行完 if (res instanceof Promise) await res; } catch (e) { this.throwPluginError('onMounted 发生错误: \n' + (0, utils_1.stringifyError)(e)); } this.debug('add all OEBot events listeners'); // 插件监听 OEBot 的所有事件 events_1.OEEvents.forEach((evt) => { const handler = (e) => { this.emit(evt, e); }; // 插件收到事件时,将事件及数据 emit 给插件里定义的处理函数 bot.on(evt, handler); // 收集监听函数 this.addHandler(evt, handler); }); this.debug('add all oicq events listeners'); // 插件监听 oicq 的所有事件 events_1.OicqEvents.forEach((evt) => { const handler = (e) => { if (events_1.MessageEvents.includes(evt)) { const event = e; if (this.isTargetOn(event)) { this.emit(evt, event); } } else { this.emit(evt, e); } }; // 插件收到事件时,将事件及数据 emit 给插件里定义的处理函数 bot.on(evt, handler); // 收集监听函数 this.addHandler(evt, handler); }); return this; } /** * **插件请勿调用**,OEBot 框架调用此函数禁用插件 * @param {Client} bot oicq Client 实例 * @param {AdminArray} admins 框架管理员列表 */ async unmountOEBotClient(bot, admins) { this.debug('unmountOEBotClient'); // 取消监听框架管理变动 bot.off('oe.admins', this.adminChangeHandler); try { this.debug('_unmounted'); // 调用 onUnmounted 挂载的函数 const res = this._unmounted(bot, admins); // 如果是 Promise 等待其执行完 if (res instanceof Promise) await res; } catch (e) { this.throwPluginError('onUnmounted 发生错误: \n' + (0, utils_1.stringifyError)(e)); } this.removeAllHandler(); this.clearCronTasks(); this.bot = null; } /** * 从插件数据目录加载保存的数据(储存为 JSON 格式,读取为普通 JS 对象),配置不存在时返回默认值,默认为 {} 空对象 * @param {string} filepath 保存文件路径,默认为插件数据目录下的 `config.json` * @param {any} defaultValue 不存在时的默认值 * @param {fs.ReadOptions | undefined} options 加载配置的选项 * @return {any} 读取到的数据 */ loadConfig(filepath = node_path_1.default.join(this.dataDir, 'config.json'), defaultValue = {}, options = {}) { this.debug('loadConfig'); if (fs_extra_1.default.existsSync(filepath)) { try { return fs_extra_1.default.readJsonSync(filepath, options); } catch (e) { this.throwPluginError('读取插件配置出错,路径: ' + filepath); } } else { return defaultValue; } } /** * 将数据保存到插件数据目录(传入普通 JS 对象,储存为 JSON 格式) * @param {any} data 待保存的普通 JS 对象 * @param {string} filepath 保存文件路径,默认为,默认为插件数据目录下的 `config.json` * @param {fs.ReadOptions | undefined} options 写入配置的选项 * @return {boolean} 是否写入成功 */ saveConfig(data, filepath = node_path_1.default.join(this.dataDir, 'config.json'), options = {}) { this.debug('saveConfig'); try { fs_extra_1.default.writeJsonSync(filepath, data, { spaces: 2, ...options }); return true; } catch (e) { this.throwPluginError('写入插件配置出错,路径: ' + filepath); return false; } } /** * 添加消息监听函数,包括好友私聊、群消息以及讨论组消息,通过 `message_type` 判断消息类型。 * @param {MessageHandler} handler 消息处理函数,包含群消息,讨论组消息和私聊消息 */ onMessage(handler) { this.checkMountStatus(); const oicqHandler = (e) => { if (this.isTargetOn(e)) { handler(e, this.bot); } }; this.bot.on('message', oicqHandler); this.addHandler('message', oicqHandler); } /** * 添加群聊消息监听函数,等价于 plugin.on('message.group', handler) 。 * @param {GroupMessageHandler} handler 群聊消息处理函数 */ onGroupMessage(handler) { this.checkMountStatus(); const oicqHandler = (e) => { if (this.isTargetOn(e)) { handler(e, this.bot); } }; this.bot.on('message.group', oicqHandler); this.addHandler('message.group', oicqHandler); } /** * 添加私聊消息监听函数,等价于 plugin.on('message.private', handler) 。 * @param {PrivateMessageHandler} handler 私聊消息处理函数 */ onPrivateMessage(handler) { this.checkMountStatus(); const oicqHandler = (e) => { if (this.isTargetOn(e)) { handler(e, this.bot); } }; this.bot.on('message.private', oicqHandler); this.addHandler('message.private', oicqHandler); } /** * 消息匹配函数,传入字符串或正则,或字符串和正则的数组,进行精确匹配,匹配成功则调用函数 * @param {string | RegExp | (string | RegExp)[]} matches 待匹配的内容,字符串或者正则,对整个消息进行匹配 * @param {MessageHandler} handler 消息处理函数,包含群消息,讨论组消息和私聊消息 */ onMatch(matches, handler) { this.checkMountStatus(); const matchList = (0, utils_1.ensureArray)(matches); const oicqHandler = (e) => { if (this.isTargetOn(e)) { const msg = e.toString(); for (const match of matchList) { const isReg = match instanceof RegExp; const hitReg = isReg && match.test(msg); const hitString = !isReg && match === msg; if (hitReg || hitString) { handler(e, this.bot); break; } } } }; this.bot.on('message', oicqHandler); this.addHandler('message', oicqHandler); } /** * 管理员消息匹配函数,传入字符串或正则,或字符串和正则的数组,进行精确匹配,匹配成功则调用函数 * @param {string | RegExp | (string | RegExp)[]} matches 待匹配的内容,字符串或者正则,对整个消息进行匹配 * @param {MessageHandler} handler 消息处理函数,包含群消息,讨论组消息和私聊消息 */ onAdminMatch(matches, handler) { this.checkMountStatus(); const matchList = (0, utils_1.ensureArray)(matches); const oicqHandler = (e) => { if (this.isTargetOn(e)) { if (this.admins.includes(e.sender.user_id)) { const msg = e.toString(); for (const match of matchList) { const isReg = match instanceof RegExp; const hitReg = isReg && match.test(msg); const hitString = !isReg && match === msg; if (hitReg || hitString) { handler(e, this.bot); break; } } } } }; this.bot.on('message', oicqHandler); this.addHandler('message', oicqHandler); } /** * 添加命令监听函数,通过 `message_type` 判断消息类型。如果只需要监听特定的消息类型,请使用 `on` 监听,比如 `on('message.group')` * @param {string | RegExp | (string | RegExp)[]} cmds 监听的命令,可以是字符串或正则表达式,或字符串和正则的数组 * @param {MessageCmdHandler} handler 消息处理函数,包含群消息,讨论组消息和私聊消息 */ onCmd(cmds, handler) { this.checkMountStatus(); const oicqHandler = (e) => { if (this.isTargetOn(e)) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { _: params, '--': __, ...options } = (0, minimist_1.default)(e.toString().trim().split(/\s+/)); const inputCmd = params.shift() ?? ''; const cmdList = (0, utils_1.ensureArray)(cmds); for (const cmd of cmdList) { const isReg = cmd instanceof RegExp; const hitReg = isReg && cmd.test(inputCmd); const hitString = !isReg && cmd === inputCmd; if (hitReg || hitString) { handler(e, params, options); break; } } } }; this.bot.on('message', oicqHandler); this.addHandler('message', oicqHandler); } /** * 添加管理员命令监听函数,通过 `message_type` 判断消息类型。如果只需要监听特定的消息类型,请使用 `on` 监听,比如 `on('message.group')` * @param {string | RegExp | (string | RegExp)[]} cmds 监听的命令,可以是字符串或正则表达式,或字符串和正则的数组 * @param {MessageCmdHandler} handler 消息处理函数,包含群消息,讨论组消息和私聊消息 */ onAdminCmd(cmds, handler) { this.checkMountStatus(); const oicqHandler = (e) => { if (this.isTargetOn(e)) { if (this.admins.includes(e.sender.user_id)) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { _: params, '--': __, ...options } = (0, minimist_1.default)(e.toString().trim().split(/\s+/)); const inputCmd = params.shift() ?? ''; const cmdList = (0, utils_1.ensureArray)(cmds); for (const cmd of cmdList) { const isReg = cmd instanceof RegExp; const hitReg = isReg && cmd.test(inputCmd); const hitString = !isReg && cmd === inputCmd; if (hitReg || hitString) { handler(e, params, options); break; } } } } }; this.bot.on('message', oicqHandler); this.addHandler('message', oicqHandler); } /** * 插件被启用时执行,所有的插件实例调用相关的逻辑请写到传入的函数里 * @param {BotHandler} func 插件被挂载后的执行函数 */ onMounted(func) { this.debug('onMounted'); this._mounted = func; } /** * 插件被禁用时执行,所有的插件实例调用相关的逻辑请写到传入的函数里 * @param {BotHandler} func 插件被取消挂载后的执行函数 */ onUnmounted(func) { this.debug('onUnmounted'); this._unmounted = func; } /** * 打印消息到控制台 */ log(...args) { const mapFn = (e) => (typeof e === 'object' ? JSON.stringify(e, null, 2) : e); const msg = args.map(mapFn).join(', '); this.logger.log(`${this.name}: ${msg}`); } /** * 打印消息到控制台,用于插件调试,仅在 debug 以及更低的 log lever 下可见 */ debug(...args) { const mapFn = (e) => (typeof e === 'object' ? JSON.stringify(e, null, 2) : e); const msg = args.map(mapFn).join(', '); this.logger.debug(`${this.name}: ${msg}`); } /** * 添加定时任务,插件禁用时会自动清理,无需手动处理 * * @param {string} cronExpression crontab 表达式, [秒], 分, 时, 日, 月, 星期 * @param {BotHandler} fn 定时触发的函数 * @return {ScheduledTask} 定时任务 Task 实例 */ cron(cronExpression, fn) { this.checkMountStatus(); // 检验 cron 表达式有效性 const isSyntaxOK = node_cron_1.default.validate(cronExpression); if (!isSyntaxOK) { this.throwPluginError('无效的 cron 表达式'); } // 创建 cron 任务 const task = node_cron_1.default.schedule(cronExpression, () => fn(this.bot, this._admins)); this._cronTasks.push(task); return task; } /** * 检测是否已经挂载 bot 实例,未挂载抛出插件错误 */ checkMountStatus() { if (!this.bot) { this.throwPluginError('Bot 实例此时还未挂载。请在 onMounted 与 onUnmounted 中进行调用。'); } } /** * 框架管理变动事件处理函数 */ adminChangeHandler(event) { this._admins = event.admins; } /** * 添加监听函数 */ addHandler(eventName, handler) { const handlers = this._handlers.get(eventName); this._handlers.set(eventName, handlers ? [...handlers, handler] : [handler]); } /** * 取消所有监听 */ removeAllHandler() { this.debug('removeAllHandler'); for (const [eventName, handlers] of this._handlers) { handlers.forEach((handler) => this.bot.off(eventName, handler)); } } /** * 清理所有定时任务 */ clearCronTasks() { this.debug('clearCronTasks'); this._cronTasks.forEach((task) => task.stop()); } /** 目标群或者好友是否被启用,讨论组当作群聊处理 */ isTargetOn(event) { const { enableFriends, enableGroups } = this.config; const isPrivate = event.message_type === 'private'; const isGroup = event.message_type === 'group'; const isDiscuss = event.message_type === 'discuss'; const isUserEnable = isPrivate && enableFriends?.includes(event.sender.user_id); const isGroupEnable = (isGroup && enableGroups?.includes(event.group_id)) || (isDiscuss && enableGroups?.includes(event.discuss_id)); // TODO if语句简化 if (isPrivate && (!enableFriends || isUserEnable)) { return true; } if (!isPrivate && (!enableGroups || isGroupEnable)) { return true; } return false; } } exports.OEPlugin = OEPlugin;