diff --git a/src/App.ts b/src/App.ts index 6b9970d..0698c1c 100644 --- a/src/App.ts +++ b/src/App.ts @@ -7,6 +7,7 @@ import { ChannelManager } from './ChannelManager'; import { ChannelConfig, Config } from './Config'; import { EventManager } from './EventManager'; import { CommonSendMessage } from './message/Message'; +import { PluginManager } from './PluginManager'; import { ProviderManager } from './ProviderManager'; import { RestfulApiManager } from './RestfulApiManager'; import { RobotManager } from './RobotManager'; @@ -24,6 +25,7 @@ export default class App { public service!: ServiceManager; public subscribe!: SubscribeManager; public channel!: ChannelManager; + public plugin!: PluginManager; public restfulApi!: RestfulApiManager; constructor(configFile: string) { @@ -40,6 +42,7 @@ export default class App { await this.initServiceManager(); await this.initSubscribeManager(); await this.initChannelManager(); + await this.initPluginManager(); console.log('初始化完成,正在接收消息'); } @@ -77,10 +80,6 @@ export default class App { await this.subscribe.initialize(); } - async initCommandManager() { - - } - async initChannelManager() { this.channel = new ChannelManager(this, this.config.channel_config_path); @@ -94,9 +93,14 @@ export default class App { await this.channel.initialize(); } + async initPluginManager() { + this.plugin = new PluginManager(this, this.config.plugin_path); + await this.plugin.initialize(); + } + /** * 获取服务 - * @param serviceName + * @param serviceName 服务名称 * @returns */ getService(serviceName: string): T { @@ -122,14 +126,6 @@ export default class App { this.robot.sendPushMessage(channelId, messages); } - /** - * 发送消息 - * @param message - */ - async sendMessage(message: CommonSendMessage) { - - } - require(file: string): any { return require(path.join(this.srcPath, file)); } diff --git a/src/ChannelManager.ts b/src/ChannelManager.ts index acc0446..2501c7a 100644 --- a/src/ChannelManager.ts +++ b/src/ChannelManager.ts @@ -12,8 +12,6 @@ import { ChannelConfig } from './Config'; export class ChannelManager extends EventEmitter { private app: App; private channelPath: string; - private loadChannelCallback: (file: string) => any; - private removeChannelCallback: (file: string) => any; private setLoading?: debounce; private watcher!: chokidar.FSWatcher; @@ -27,9 +25,6 @@ export class ChannelManager extends EventEmitter { this.channelPath = channelPath; this.channels = {}; this.channelName = {}; - - this.loadChannelCallback = this.loadChannel.bind(this); - this.removeChannelCallback = this.removeChannel.bind(this); } /** @@ -42,9 +37,9 @@ export class ChannelManager extends EventEmitter { persistent: true }); - this.watcher.on('add', this.loadChannelCallback); - this.watcher.on('change', this.loadChannelCallback); - this.watcher.on('unlink', this.removeChannelCallback); + this.watcher.on('add', this.loadChannel.bind(this)); + this.watcher.on('change', this.loadChannel.bind(this)); + this.watcher.on('unlink', this.loadChannel.bind(this)); } /** diff --git a/src/Config.ts b/src/Config.ts index 10da03a..0e8d2d6 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -3,6 +3,7 @@ import { RegexFilterConfig } from "./generator/RegexFilter"; export type Config = { channel_config_path: string; + plugin_path: string; subscribe_config: string; debug: boolean; robot: { [key: string]: RobotConfig }; diff --git a/src/EventManager.ts b/src/EventManager.ts index f8f7e55..d7a6ef3 100644 --- a/src/EventManager.ts +++ b/src/EventManager.ts @@ -1,253 +1,174 @@ -import EventEmitter from "events"; -import { debounce } from "throttle-debounce"; import App from "./App"; -import { CommonReceivedMessage } from "./message/Message"; +import { CommonReceivedMessage, CommonSendMessage } from "./message/Message"; import { GroupSender, UserSender } from "./message/Sender"; +import { ControllerSubscribeSource, MessageEventOptions, MessagePriority, PluginController } from "./PluginManager"; import { Robot } from "./RobotManager"; +export type PluginControllerListenerInfo = { + priority: number; + callback: CallableFunction; + controller: PluginController; +} + export class EventManager { private app: App; - private eventEmitter: EventEmitter; - private eventGroup: Record = {}; - private eventHandlerList: Record = {}; + private eventSortDebounce: Record = {}; + private eventList: Record = {}; constructor(app: App) { this.app = app; - this.eventEmitter = new EventEmitter; } public async initialize() { - - } - - public getEventGroup() { - + } -} -export const MessagePriority = { - LOWEST: 0, - LOW: 20, - DEFAULT: 40, - /** - * 在控制器中添加的临时会话处理器。 - * 用于处理深层会话,会比一般指令优先级高。 - */ - TEMP_HANDLER: 60, - HIGH: 80, - /** - * 一些系统自带的事件处理,如:记录消息 - */ - SYSTEM: 90, - /** - * 最高优先级,可以覆盖系统原本的处理 - */ - HIGHEST: 100 -}; - -export type MessageEventOptions = { - priority?: number, - /** Base sources: private, group, channel */ - source?: string[], - robotApi?: string[], -}; - -export type CommandInfo = { - command: string, - name: string, - alias?: string[], - help?: string, -}; - -export type EventListenerInfo = { - priority: number; - callback: CallableFunction; -} - -export type MessageCallback = (message: CommonReceivedMessage, resolved: VoidFunction) => any; -export type CommandCallback = (argv: string[], message: CommonReceivedMessage, resolved: VoidFunction) => any; -export type RawEventCallback = (robot: Robot, event: any, resolved: VoidFunction) => any; - -export type AllowedList = string[] | '*'; - -export class EventGroup { - readonly id: string; + on(event: string, controller: PluginController, callback: CallableFunction, options?: MessageEventOptions) { + if (!(event in this.eventList)) { + this.eventList[event] = []; + } - public allowPrivate = true; - public allowGroup = true; + let defaultOptions: MessageEventOptions = { + priority: MessagePriority.DEFAULT + }; + if (!options) { + options = defaultOptions; + } else { + options = { + ...defaultOptions, + ...options + }; + } - public allowedGroupList: AllowedList = '*'; + const eventInfo = { + callback: callback, + priority: options.priority!, + controller: controller + }; - private commandList: CommandInfo[] = []; - private eventList: Record = {}; + this.eventList[event].push(eventInfo); - constructor(id: string) { - this.id = id; + this.sortEvent(event); } - public shouldAllowSource: (sender: any) => boolean = (sender) => { - if (sender instanceof UserSender) { - if (this.allowPrivate) { - return true; + off(event: string, controller: PluginController, callback: CallableFunction): void + off(controller: PluginController): void + off(...args: any): void { + if (typeof args[0] === 'string') { + let [event, controller, callback] = args; + if (Array.isArray(this.eventList[event])) { + this.eventList[event] = this.eventList[event].filter((eventInfo) => eventInfo.callback !== callback || eventInfo.controller !== controller); } - } else if (sender instanceof GroupSender) { - if (this.allowedGroupList === '*') { - return true; - } else if (this.allowedGroupList.includes(sender.groupId)) { - return true; + } else if (typeof args[0] !== 'undefined') { + let controller = args[0]; + for (let event in this.eventList) { + this.eventList[event] = this.eventList[event].filter((eventInfo) => eventInfo.controller !== controller); } } - return false; } - public emit(eventName: string, ...args: any[]) { + public async emit(eventName: string, senderInfo: ControllerSubscribeSource, ...args: any[]) { const eventList = this.eventList[eventName]; + if (!eventList) return false; let isResolved = false; - let isBreakAll = false; - const resolved = (breakAll: boolean = false) => { + const resolved = () => { isResolved = true; - if (breakAll) { - isBreakAll = true; - } }; for (let eventInfo of eventList) { - eventInfo.callback(...args, resolved); - if (isResolved) { - break; + if (eventInfo.controller.autoSubscribe) { + if (!eventInfo.controller.isAllowSubscribe(senderInfo)) { + continue; + } else { + // 需要添加订阅检测 + } + } else { + // 需要添加订阅检测 + continue; + } + + try { + await eventInfo.callback(...args, resolved); + if (isResolved) { + break; + } + } catch(err: any) { + console.error(`事件 ${eventName} 处理失败`); + console.error(err); } } - return !isBreakAll; + return isResolved; } - on(event: string, callback: CallableFunction, options?: MessageEventOptions) { - if (!(event in this.eventList)) { - this.eventList[event] = []; - } + public async emitMessage(message: CommonReceivedMessage) { + let isResolved = false; - let defaultOptions: MessageEventOptions = { - priority: MessagePriority.DEFAULT - }; - if (!options) { - options = defaultOptions; - } else { - options = { - ...defaultOptions, - ...options - }; + if (message.origin === 'private' || (message.origin === 'group' && message.mentionedReceiver)) { + isResolved = await this.emit(`message/focused`, this.getSenderInfo(message), message); + if (isResolved) return true; } - const eventInfo = { - callback: callback, - priority: options.priority! - }; - const singleEventList = this.eventList[event]; - const priority = options.priority!; + isResolved = await this.emit(`message/${message.origin}`, this.getSenderInfo(message), message); + if (isResolved) return true; - // Add event to specified position - if (singleEventList.length === 0) { - singleEventList.push(eventInfo); - } else { - for (let i = 0; i < singleEventList.length; i++) { - if (singleEventList[i].priority < priority) { - const target = i - 1; - if (target === 0) { - singleEventList.unshift(eventInfo); - } else { - this.eventList[event] = [ - ...singleEventList.slice(0, target), - eventInfo, - ...singleEventList.slice(target) - ]; - } - } - } - } - } - - addCommand(command: string, name: string, callback: MessageCallback, options?: MessageEventOptions): void - addCommand(commandInfo: CommandInfo, callback: MessageCallback, options?: MessageEventOptions): void - addCommand(...args: any[]): void { - // 处理传入参数 - let commandInfo: Partial = {}; - let callback: MessageCallback; - let options: MessageEventOptions; - if (typeof args[0] === 'string' && typeof args[1] === 'string') { - commandInfo = { - command: args[0], - name: args[1] - }; - callback = args[2]; - options = args[3] ?? {}; - } else { - commandInfo = args[0]; - callback = args[1]; - options = args[2] ?? {}; - } + isResolved = await this.emit('message', this.getSenderInfo(message), message); + if (isResolved) return true; - // 注册消息 - this.commandList.push(commandInfo as any); - this.on(`command/${commandInfo.command}`, callback, options); - if (Array.isArray(commandInfo.alias)) { // Add event for alias - commandInfo.alias.forEach((cmd) => { - this.on(`command/${cmd}`, callback, options); - }); - } + return false; } - /** - * Add message handle. - * will be trigger on private message or group message with mentions to robot - * @param callback - * @param options - */ - onMentionedMessage(callback: MessageCallback, options?: MessageEventOptions) { - this.on('mentionedMessage', callback, options); - this.on('privateMessage', callback, options); + public async emitCommand(command: string, args: string, message: CommonReceivedMessage) { + return await this.emit(`command/${command}`, this.getSenderInfo(message), args, message); } - /** - * Add private message handle. - * @param callback - * @param options - */ - onPrivateMessage(callback: MessageCallback, options?: MessageEventOptions) { - this.on('privateMessage', callback, options); + public async emitRawEvent(robot: Robot, event: string, ...args: any[]) { + return await this.emit(`raw/${robot.type}/${event}`, { type: 'raw', robot: robot }, event); } - /** - * Add group message handle. - * @param callback - * @param options - */ - onGroupMessage(callback: MessageCallback, options?: MessageEventOptions) { - this.on('groupMessage', callback, options); + public async emitRawMessage(message: CommonReceivedMessage) { + let isResolved = false; + isResolved = await this.emit(`raw/${message.receiver.type}/message`, this.getSenderInfo(message), message); + return await this.emit('raw/message', this.getSenderInfo(message), message); } - /** - * Add message handle. - * Will handle all messages in group - * @param callback - * @param options - */ - onMessage(callback: MessageCallback, options?: MessageEventOptions) { - this.on('message', callback, options); + public async emitFilterSendMessage(message: CommonSendMessage) { + } - /** - * Add raw message handle. - * Will be triggered even when the message is a command. - * @param callback - * @param options - */ - onRawMessage(callback: MessageCallback, options?: MessageEventOptions) { - this.on('rawMessage', callback, options); + public getSenderInfo(message: CommonReceivedMessage): ControllerSubscribeSource { + if (message.origin === 'private') { + return { + type: 'private', + robot: message.receiver, + userId: message.sender.uid + }; + } else if (message.origin === 'group') { + return { + type: 'group', + robot: message.receiver, + groupId: message.sender.groupId, + userId: message.sender.uid + }; + } + + return { + type: 'unknown', + robot: message.receiver + } } - onRawEvent(callback: RawEventCallback, options?: MessageEventOptions) { - this.on('rawEvent', callback, options); + private sortEvent(eventName: string) { + if (this.eventSortDebounce[eventName]) { + return; + } + + this.eventSortDebounce[eventName] = setTimeout(() => { + this.eventList[eventName] = this.eventList[eventName].sort((a, b) => b.priority - a.priority); + + delete this.eventSortDebounce[eventName]; + }, 200); } -} \ No newline at end of file +} diff --git a/src/PluginManager.ts b/src/PluginManager.ts new file mode 100644 index 0000000..fbbd0e1 --- /dev/null +++ b/src/PluginManager.ts @@ -0,0 +1,355 @@ +import { EventManager } from "./EventManager"; +import { CommonReceivedMessage } from "./message/Message"; +import { Robot } from "./RobotManager"; +import chokidar from 'chokidar'; +import App from "./App"; +import EventEmitter from "events"; +import path from "path"; + +export const MessagePriority = { + LOWEST: 0, + LOW: 20, + DEFAULT: 40, + /** + * 在控制器中添加的临时会话处理器。 + * 用于处理深层会话,会比一般指令优先级高。 + */ + TEMP_HANDLER: 60, + HIGH: 80, + /** + * 一些系统自带的事件处理,如:记录消息 + */ + SYSTEM: 90, + /** + * 最高优先级,可以覆盖系统原本的处理 + */ + HIGHEST: 100 +}; + +export type MessageEventOptions = { + priority?: number, +}; + +export type CommandInfo = { + command: string, + name: string, + alias?: string[], + help?: string, +}; + +export type EventListenerInfo = { + priority: number; + callback: CallableFunction; +} + +export type MessageCallback = (message: CommonReceivedMessage, resolved: VoidFunction) => any; +export type CommandCallback = (args: string, message: CommonReceivedMessage, resolved: VoidFunction) => any; +export type RawEventCallback = (robot: Robot, event: any, resolved: VoidFunction) => any; + +export type AllowedList = string[] | '*'; + +export type ControllerSubscribeSource = { + type: 'private' | 'group' | 'channel' | 'raw' | string, + robot: Robot, + groupId?: string, + userId?: string, + channelId?: string, +} + +export class PluginManager extends EventEmitter { + private app: App; + private pluginPath: string; + + private watcher!: chokidar.FSWatcher; + public controllers: Record; + public fileControllers: Record; + + constructor(app: App, pluginPath: string) { + super(); + + this.app = app; + this.pluginPath = pluginPath; + this.controllers = {}; + this.fileControllers = {}; + } + + /** + * 加载所有Controllers + */ + async initialize() { + this.watcher = chokidar.watch(this.pluginPath, { + ignored: '*.bak', + ignorePermissionErrors: true, + persistent: true + }); + + this.watcher.on('add', this.loadController.bind(this)); + this.watcher.on('change', this.loadController.bind(this)); + this.watcher.on('unlink', this.removeController.bind(this)); + } + + async loadController(file: string) { + if (!file.match(/\.m?js$/)) return; + + let moduleName = path.resolve(file).replace(/\\/g, '/').replace(/\.m?js$/, ''); + + try { + const controller = await import(moduleName); + if (controller) { + const controllerClass = controller.default ?? controller; + const controllerInstance: PluginController = new controllerClass(this.app, this.app.event); + if (controllerInstance.id && controllerInstance.id !== '') { + const controllerId = controllerInstance.id; + + let isReload = false; + if (controllerId in this.controllers) { + // Reload plugin + isReload = true; + await this.removeController(file, true); + } + this.controllers[controllerId] = controllerInstance; + this.fileControllers[file] = controllerInstance; + + if (isReload) { + console.log(`已重新加载Controller: ${file}`); + this.emit('controllerReloaded', controllerInstance); + } else { + console.log(`已加载Controller: ${file}`); + this.emit('controllerLoaded', controllerInstance); + } + + await controllerInstance.initialize(); + } else { + throw new Error('PluginController ID is not defined.'); + } + } else { + throw new Error('PluginController does not have an export.'); + } + } catch(err: any) { + console.error(`加载Controller失败: ${file}`); + console.error(err); + } + } + + async removeController(file: string, isReload = false) { + const controller = this.fileControllers[file]; + if (controller) { + await controller.destroy(); + + delete this.controllers[file]; + delete this.fileControllers[file]; + this.emit('controllerRemoved', controller); + + if (!isReload) { + console.log(`已移除Controller: ${controller.id}`); + } + } + } +} + +export class PluginController { + public id: string = ''; + public name: string = '未命名功能'; + public description: string = ''; + + private app: App; + private eventManager: EventManager; + + public autoSubscribe = false; + public forceSubscribe = false; + public showInSubscribeList = true; + + public allowPrivate = true; + public allowGroup = true; + public allowChannel = true; + + public allowedRobotTypeList: AllowedList = '*'; + + private commandList: CommandInfo[] = []; + private eventList: Record = {}; + + constructor(app: App, eventManager: EventManager) { + this.app = app; + this.eventManager = eventManager; + } + + public isAllowSubscribe: (source: ControllerSubscribeSource) => boolean = (source) => { + if (this.allowedRobotTypeList !== '*' && !this.allowedRobotTypeList.includes(source.robot.type)) { + return false; + } + + switch (source.type) { + case 'private': + if (!this.allowPrivate) { + return false; + } + break; + case 'group': + if (!this.allowGroup) { + return false; + } + break; + case 'channel': + if (!this.allowChannel) { + return false; + } + break; + } + return true; + } + + /** + * Add private message handler. + * @param event Event name + * @param callback Callback function + * @param options Options + */ + protected on(event: 'message/private', callback: MessageCallback, options?: MessageEventOptions): void + /** + * Add group message handler. + * @param event Event name + * @param callback Callback function + * @param options Options + */ + protected on(event: 'message/group', callback: MessageCallback, options?: MessageEventOptions): void + /** + * Add channel message handler. + * @param event Event name + * @param callback Callback function + * @param options Options + */ + protected on(event: 'message/channel', callback: MessageCallback, options?: MessageEventOptions): void + /** + * Add message handle. + * will be trigger on private message or group message with mentions to robot + * @param event Event name + * @param callback Callback function + * @param options Options + */ + protected on(event: 'message/focused', callback: MessageCallback, options?: MessageEventOptions): void + /** + * Add message handler. + * Will handle all messages (group, private, channel) + * @param event Event name + * @param callback Callback function + * @param options Options + */ + protected on(event: 'message', callback: MessageCallback, options?: MessageEventOptions): void + /** + * Add raw message handler. + * Will be triggered even when the message is a command. + * @param event Event name + * @param callback Callback function + * @param options Options + */ + protected on(event: 'raw/message', callback: MessageCallback, options?: MessageEventOptions): void + /** + * Add robot raw event handler. + * @param event Event name + * @param callback Callback function + * @param options Options + */ + protected on(event: 'raw/event', callback: RawEventCallback, options?: MessageEventOptions): void + /** + * Add other event handler. + * @param event Event name + * @param callback Callback function + * @param options Options + */ + protected on(event: string, callback: CallableFunction, options?: MessageEventOptions): void + /** + * Add event handler. + * @param event Event name + * @param callback Callback function + * @param options Options + */ + protected on(event: string, callback: CallableFunction, options?: MessageEventOptions): void { + if (!(event in this.eventList)) { + this.eventList[event] = []; + } + + let defaultOptions: MessageEventOptions = { + priority: MessagePriority.DEFAULT + }; + if (!options) { + options = defaultOptions; + } else { + options = { + ...defaultOptions, + ...options + }; + } + + const eventInfo = { + callback: callback, + priority: options.priority! + }; + this.eventList[event].push(eventInfo); + + this.eventManager.on(event, this, callback, options); + } + + protected off(event: string, callback: CallableFunction): void { + if (Array.isArray(this.eventList[event])) { + this.eventList[event] = this.eventList[event].filter((eventInfo) => { + return eventInfo.callback !== callback; + }); + } + + this.eventManager.off(event, this, callback); + } + + /** + * Register command. + * @param command + * @param name + * @param callback + * @param options + */ + protected registerCommand(command: string, name: string, callback: CommandCallback, options?: MessageEventOptions): void + protected registerCommand(commandInfo: CommandInfo, callback: CommandCallback, options?: MessageEventOptions): void + protected registerCommand(...args: any[]): void { + // 处理传入参数 + let commandInfo: Partial = {}; + let callback: MessageCallback; + let options: MessageEventOptions; + if (typeof args[0] === 'string' && typeof args[1] === 'string') { + commandInfo = { + command: args[0], + name: args[1] + }; + callback = args[2]; + options = args[3] ?? {}; + } else { + commandInfo = args[0]; + callback = args[1]; + options = args[2] ?? {}; + } + + // 注册消息事件 + this.commandList.push(commandInfo as any); + this.on(`command/${commandInfo.command}`, callback, options); + if (Array.isArray(commandInfo.alias)) { // Add event for alias + commandInfo.alias.forEach((cmd) => { + this.on(`command/${cmd}`, callback, options); + }); + } + } + + /** + * Initialize plugin controller. + */ + public async initialize() { + + } + + /** + * Destroy eventGroup. + * Will remove all event listeners. + */ + public async destroy() { + this.eventManager.off(this); + + this.eventList = {}; + } +} \ No newline at end of file diff --git a/src/RobotManager.ts b/src/RobotManager.ts index 37b05f3..13a3d78 100644 --- a/src/RobotManager.ts +++ b/src/RobotManager.ts @@ -5,6 +5,7 @@ import App from "./App"; import { MultipleMessage } from "./base/provider/BaseProvider"; import { RobotConfig } from "./Config"; import { CommonSendMessage } from "./message/Message"; +import { CommandInfo } from "./PluginManager"; import { RestfulApiManager, RestfulContext, RestfulRouter } from "./RestfulApiManager"; import { Target } from "./SubscribeManager"; @@ -16,6 +17,7 @@ export interface Robot { uid?: string; initialize?: () => Promise; initRestfulApi?: (router: RestfulRouter, api: RestfulApiManager) => Promise; + setCommands?(commands: CommandInfo[]): Promise; sendMessage(message: CommonSendMessage): Promise; sendPushMessage(targets: Target[], message: string): Promise; } @@ -38,9 +40,9 @@ export class RobotManager { async initialize() { for (let file of fs.readdirSync(ROBOT_PATH)) { let robotFile = `${ROBOT_PATH}/${file}`; - if (robotFile.match(/\.(js|mjs)$/)) { + if (robotFile.match(/\.m?js$/)) { // 加载js文件 - let robotName = path.basename(robotFile).replace(/Robot\.(js|mjs)$/gi, "").toLocaleLowerCase(); + let robotName = path.basename(robotFile).replace(/Robot\.m?js$/gi, "").toLocaleLowerCase(); try { let robotClass = require(robotFile)?.default; if (!robotClass) { diff --git a/src/controller/EchoController.ts b/src/controller/EchoController.ts index 5216644..0efc81e 100644 --- a/src/controller/EchoController.ts +++ b/src/controller/EchoController.ts @@ -1,21 +1,27 @@ -import { CommonGroupMessage, CommonReceivedMessage, CommonSendMessage } from "../message/Message"; -import { GroupSender } from "../message/Sender"; +import { CommonReceivedMessage } from "../message/Message"; +import { PluginController } from "../PluginManager"; -export class EchoController { - fliterBotGroupMessage(message: CommonGroupMessage) { - let newMsg = message.content.map((chunk) => { - if (chunk.type === 'text') { - return { - type: 'text', - data: { - text: chunk.data.text.replace(/^[ ]*说[::, ]*/, '').replace(/我/g, '你'), - } - }; +export default class EchoController extends PluginController { + public id = 'echo'; + public name = '复读机'; + public description = '友好地复读消息'; + + public autoSubscribe = true; + + public async initialize(): Promise { + this.on("message/focused", this.handleEcho); + } + + private async handleEcho(message: CommonReceivedMessage, resolved: CallableFunction) { + if (message.contentText.match(/^说(,|:| )/)) { + resolved(); + + let repliedMessage = message.contentText.replace(/^说(,|:| )/, ""); + if (repliedMessage.match(/我/g)) { + message.sendReply("您说" + repliedMessage.replace(/我/g, "您"), true); } else { - return chunk; + message.sendReply(repliedMessage, true); } - }); - - message.sendReply(newMsg); + } } } \ No newline at end of file diff --git a/src/message/Message.ts b/src/message/Message.ts index 59920ba..bdd07a0 100644 --- a/src/message/Message.ts +++ b/src/message/Message.ts @@ -59,10 +59,27 @@ export class CommonMessage { type: string | CommonMessageType = "text"; origin: string | CommonMessageOrigin = "private"; /** 回复的消息ID */ - replyId?: string; + repliedId?: string; /** 提到的人 */ mentions?: { uid: string, text?: string }[]; + private _contentText?: string; + + public get contentText() { + if (typeof this._contentText === 'undefined') { + this._contentText = this.content.map((chunk) => { + if (chunk.type === 'text') { + return chunk.data.text; + } else if (chunk.type === 'mention') { + return '[@' + (chunk.data.text || chunk.data.uid) + ']'; + } else { + return JSON.stringify([chunk.type, chunk.data]); + } + }).join('').trim(); + } + return this._contentText; + } + /** * 提到某人 * @param uid 用户ID @@ -113,29 +130,99 @@ export class CommonMessage { return true; } } + + /** + * 合并文本消息 + * @returns + */ + public combineText() { + let newContent: MessageChunk[] = []; + let lastText: string | undefined; + + this.content.forEach((chunk) => { + if (chunk.type === 'text') { + if (!lastText) { + lastText = chunk.data.text; + } else { + lastText += chunk.data.text; + } + } else { + if (lastText) { + newContent.push({ + type: 'text', + data: { text: lastText } + }); + lastText = undefined; + } + newContent.push(chunk); + } + }); + + if (lastText) { + newContent.push({ + type: 'text', + data: { text: lastText } + }); + } + + this.content = newContent; + } + + /** + * 替换消息内容 + * @param content + * @param searchValue + * @param replaceValue + * @returns + */ + public static replace(content: MessageChunk[], searchValue: RegExp, replaceValue: string) { + return content.map((chunk, index) => { + if (chunk.type === 'text') { + let newText: string = chunk.data.text; + let offset = [0, 0]; + if (index === 0) { + offset[0] = 1; + newText = "\t" + newText; + } else if (index === content.length - 1) { + offset[1] = 1; + newText += "\t"; + } + newText = newText.replace(searchValue, replaceValue); + chunk.data.text = newText.substring(offset[0], newText.length - offset[1]); + } + return chunk; + }); + } } /** 基本发送的消息 */ export class CommonSendMessage extends CommonMessage { + /** 发送者 */ sender: Robot; + /** 接收方的ID */ targetId: string; - constructor(sender: Robot, targetType: string, targetId: string, content?: MessageChunk[]) { + /** 回复的消息 */ + repliedMessage?: CommonReceivedMessage; + + constructor(sender: Robot, origin: string, targetId: string, content?: MessageChunk[]) { super(); this.sender = sender; - this.type = targetType; + this.origin = origin; this.targetId = targetId; if (Array.isArray(content)) this.content = content; } } export class CommonReceivedMessage extends CommonMessage { - // 接收时间 + /** 接收时间 */ time: Date = new Date(); - // 接收者 + /** 接收者 */ receiver: Robot; - // 发送者 + /** 发送者 */ sender: any; + /** 接收者是否被提到 */ + mentionedReceiver: boolean = false; constructor(receiver: Robot, messageId?: string) { super(); @@ -146,7 +233,7 @@ export class CommonReceivedMessage extends CommonMessage { public async sendReply(message: string | MessageChunk[], addReply: boolean = false): Promise { const sender = this.sender as BaseSender; - let newMessage = new CommonSendMessage(this.receiver!, sender.type, sender.targetId); + let newMessage = new CommonSendMessage(this.receiver!, this.origin, sender.targetId); if (typeof message === 'string') { let msgContent: MessageChunk[] = [{ type: 'text', @@ -159,6 +246,11 @@ export class CommonReceivedMessage extends CommonMessage { return null; } + if (addReply) { + newMessage.repliedId = this.id; + newMessage.repliedMessage = this; + } + newMessage = await this.receiver.sendMessage(newMessage); return newMessage; @@ -166,7 +258,8 @@ export class CommonReceivedMessage extends CommonMessage { } export class CommonPrivateMessage extends CommonReceivedMessage { - sender: US; + public sender: US; + public origin = 'private'; constructor(sender: US, receiver: Robot, messageId?: string) { super(receiver, messageId); @@ -176,6 +269,7 @@ export class CommonPrivateMessage extends CommonReceivedM export class CommonGroupMessage extends CommonReceivedMessage { sender: GS; + public origin = 'group'; constructor(sender: GS, receiver: Robot, messageId?: string) { super(receiver, messageId); diff --git a/src/robot/QQRobot.ts b/src/robot/QQRobot.ts index 8fc943b..5c69644 100644 --- a/src/robot/QQRobot.ts +++ b/src/robot/QQRobot.ts @@ -11,7 +11,7 @@ import { CommonReceivedMessage, CommonSendMessage, MentionMessage, TextMessage } export type QQRobotConfig = { user: string; host: string; - baseId?: string; + command_prefix?: string; } export type QQGroupInfo = { @@ -27,6 +27,8 @@ export default class QQRobot implements Robot { public uid: string; public robotId: string; + public commandPrefix: string[] = ['/', '!', '!', '/']; + private app: App; private endpoint: string; @@ -39,6 +41,14 @@ export default class QQRobot implements Robot { this.robotId = robotId; this.endpoint = 'http://' + config.host; this.uid = config.user; + + if (config.command_prefix) { + if (Array.isArray(config.command_prefix)) { + this.commandPrefix = config.command_prefix; + } else if (typeof config.command_prefix === 'string') { + this.commandPrefix = [config.command_prefix]; + } + } } async initialize() { @@ -66,7 +76,7 @@ export default class QQRobot implements Robot { } async initRestfulApi(router: RestfulRouter, api: RestfulApiManager) { - api.router.post(`/event`, this.handlePostEvent.bind(this)); + router.post(`/event`, this.handlePostEvent.bind(this)); } async handlePostEvent(ctx: FullRestfulContext, next: koa.Next) { @@ -88,12 +98,19 @@ export default class QQRobot implements Robot { * @param postData */ async handleMessage(postData: any) { + let isResolved = false; + if (postData.type) { + isResolved = await this.app.event.emitRawEvent(this, postData.type, postData); + if (isResolved) return; + } + if (postData.message_id) { + let message: QQGroupMessage | QQPrivateMessage | undefined; if (postData.message_type === 'group') { // 处理群消息 let groupInfo = this.groupList.find((info) => info.groupId === postData.group_id); - let groupSender = new QQGroupSender(postData.group_id, postData.user_id); + let groupSender = new QQGroupSender(postData.group_id.toString(), postData.user_id.toString()); groupSender.groupInfo = groupInfo; groupSender.groupName = groupInfo?.groupName; groupSender.globalNickName = postData.sender?.nickname; @@ -102,21 +119,49 @@ export default class QQRobot implements Robot { groupSender.level = postData.sender?.level; groupSender.title = postData.sender?.title; - let message = new QQGroupMessage(groupSender, this, postData.message_id.toString()); + message = new QQGroupMessage(groupSender, this, postData.message_id.toString()); message.time = new Date(postData.time * 1000); - message = await parseQQMessageChunk(postData.message ?? [], message); + message = await parseQQMessageChunk(this, postData.message ?? [], message); } else if (postData.message_type === 'private') { // 处理私聊消息 - let userSender = new QQUserSender(postData.user_id); + let userSender = new QQUserSender(postData.user_id.toString()); userSender.nickName = postData.sender?.nickname; - let message = new QQPrivateMessage(userSender, this, postData.message_id.toString()); + message = new QQPrivateMessage(userSender, this, postData.message_id.toString()); message.time = new Date(postData.time * 1000); - message = await parseQQMessageChunk(postData.message ?? [], message); + message = await parseQQMessageChunk(this, postData.message ?? [], message); + } + + if (message) { + // 处理原始消息 + isResolved = await this.app.event.emitRawMessage(message); + if (isResolved) return; + + // 处理指令 + let commandInfo = this.getCommandInfo(message); + if (commandInfo) { + isResolved = await this.app.event.emitCommand(commandInfo.name, commandInfo.args, message); + if (isResolved) return; + } + + // 处理消息 + isResolved = await this.app.event.emitMessage(message); + if (isResolved) return; + } + } + } + + getCommandInfo(message: CommonReceivedMessage) { + for (let prefix of this.commandPrefix) { + if (message.contentText.startsWith(prefix)) { + let name = message.contentText.substring(prefix.length).split(' ')[0]; + let args = message.contentText.substring(prefix.length + name.length + 1); + return { name, args }; } } + return null; } /** @@ -168,6 +213,7 @@ export default class QQRobot implements Robot { if (message.origin === 'private') { await this.sendToUser(message.targetId, msgData); } else if (message.origin === 'group') { + console.log('发送群消息', message.targetId, msgData); await this.sendToGroup(message.targetId, msgData); } diff --git a/src/robot/TelegramRobot.ts b/src/robot/TelegramRobot.ts index f0d5bff..cd6183b 100644 --- a/src/robot/TelegramRobot.ts +++ b/src/robot/TelegramRobot.ts @@ -1,6 +1,7 @@ import TelegramBot from "node-telegram-bot-api"; import App from "../App"; import { CommonSendMessage } from "../message/Message"; +import { CommandInfo } from "../PluginManager"; import { Robot } from "../RobotManager"; import { Target } from "../SubscribeManager"; import { Utils } from "../utils/Utils"; @@ -56,6 +57,19 @@ export default class TelegramRobot implements Robot { }); } + async setCommands(commands: CommandInfo[]) { + /* + let botCommands: TelegramBot.BotCommand[] = []; + for (let command of commands) { + botCommands.push({ + command: command.command, + description: command.help ?? command.name + }); + } + await this.bot.setMyCommands(botCommands); + */ + } + /** * 发送群消息 */ diff --git a/src/robot/qq/Message.ts b/src/robot/qq/Message.ts index 27a0823..1842d46 100644 --- a/src/robot/qq/Message.ts +++ b/src/robot/qq/Message.ts @@ -1,6 +1,6 @@ import { CommonGroupMessage, CommonPrivateMessage, CommonReceivedMessage, CommonSendMessage, MentionMessage, MessageChunk, TextMessage } from "../../message/Message"; import { GroupSender, UserSender } from "../../message/Sender"; -import { QQGroupInfo } from "../QQRobot"; +import QQRobot, { QQGroupInfo } from "../QQRobot"; export interface QQFaceMessage extends MessageChunk { type: 'qqface'; @@ -66,7 +66,7 @@ export class QQGroupSender extends GroupSender { * @param message * @returns */ -export async function parseQQMessageChunk(messageData: any[], message: CommonReceivedMessage): Promise { +export async function parseQQMessageChunk(bot: QQRobot, messageData: any[], message: CommonReceivedMessage): Promise { let willIgnoreMention = false; messageData.forEach((chunkData) => { if (chunkData.type) { @@ -110,13 +110,17 @@ export async function parseQQMessageChunk(messageData: any[], message: CommonRec case 'at': if (chunkData.data?.qq) { if (!willIgnoreMention) { - message.mention(chunkData.data.qq); - message.content.push({ - type: 'mention', - data: { - uid: chunkData.data.qq - } - } as MentionMessage); + if (chunkData.data.qq == bot.uid) { // 如果是@机器人 + message.mentionedReceiver = true; + } else { // @其他人的情况 + message.mention(chunkData.data.qq); + message.content.push({ + type: 'mention', + data: { + uid: chunkData.data.qq + } + } as MentionMessage); + } } else { willIgnoreMention = false; } @@ -124,7 +128,7 @@ export async function parseQQMessageChunk(messageData: any[], message: CommonRec break; case 'reply': if (chunkData.data?.id) { - message.replyId = chunkData.data.id; + message.repliedId = chunkData.data.id; willIgnoreMention = true; // 忽略下一个“@” } break; @@ -164,7 +168,7 @@ export async function convertMessageToQQChunk(message: CommonSendMessage) { msgChunk.push({ type: 'text', data: { - url: chunk.data.url + text: chunk.data.text } }); break; @@ -204,10 +208,27 @@ export async function convertMessageToQQChunk(message: CommonSendMessage) { } }) - if (message.replyId) { + if (message.repliedId) { + if (message.origin === 'group' && message.repliedMessage?.sender.uid) { + // 目前不知道为何,@不能正常传递 + /* + msgChunk.unshift({ + type: 'text', + data: { text: ' ' } + }); + msgChunk.unshift({ + type: 'at', + data: { qq: message.repliedMessage.sender.uid } + }); + msgChunk.unshift({ + type: 'text', + data: { text: ' ' } + }); + */ + } msgChunk.unshift({ type: 'reply', - data: { id: message.replyId } + data: { id: message.repliedId } }); }