You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

401 lines
14 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import App from "./App";
import { CommandOverrideConfig } from "./Config";
import { PermissionDeniedError, RateLimitError } from "./error/errors";
import { CommonReceivedMessage } from "./message/Message";
import { ChatIdentity } from "./message/Sender";
import { CommandInfo, CommandInputArgs, EventScope, MessageEventOptions, MessagePriority, PluginEvent } from "./PluginManager";
import { Robot } from "./robot/Robot";
import { SubscribeItem } from "./SubscribeManager";
import { Reactive } from "./utils/reactive";
export type ControllerEventInfo = {
priority: number;
callback: CallableFunction;
eventScope: PluginEvent;
}
export type SessionEventInfo = {
activeTime: Date;
eventScope: EventScope;
}
export type ControllerCommandInfo = {
commandInfo: CommandInfo;
eventScope: PluginEvent;
}
export class EventManager {
private app: App;
/** 事件排序的debounce */
private eventSortDebounce: Record<string, NodeJS.Timeout> = {};
/** 全局事件列表 */
private eventList: Record<string, ControllerEventInfo[]> = {};
/** 会话事件列表 */
private sessionEventList: Record<string, EventScope> = {};
/** 全局指令列表 */
private commandList: Record<string, ControllerCommandInfo> = {};
/** 指令信息 */
private commandInfoList: ControllerCommandInfo[] = [];
/** 指令信息覆盖配置 */
private commandOverride: CommandOverrideConfig;
constructor(app: App) {
this.app = app;
this.commandOverride = app.config.command_override ?? {};
}
public async initialize() {
}
public on(event: string, eventScope: PluginEvent, callback: CallableFunction, options?: MessageEventOptions) {
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!,
eventScope
};
this.eventList[event].push(eventInfo);
this.sortEvent(event);
}
public off(event: string, eventScope: PluginEvent, callback: CallableFunction): void
public off(eventScope: PluginEvent): void
public 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.eventScope !== controller);
}
} 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.eventScope !== controller);
}
}
}
public addCommand(commandInfo: CommandInfo, eventScope: PluginEvent) {
// 如果配置了Command覆盖则覆盖原本的指令设置
if (commandInfo.command in this.commandOverride) {
commandInfo = {
...commandInfo,
...this.commandOverride[commandInfo.command]
};
}
let data = {
commandInfo,
eventScope: eventScope
};
this.commandInfoList.push(data);
this.commandList[commandInfo.command.toLocaleLowerCase()] = data;
if (Array.isArray(commandInfo.alias)) {
commandInfo.alias.forEach((alias) => {
this.commandList[alias.toLocaleLowerCase()] = data;
});
}
}
public removeCommand(commandInfo: CommandInfo): void
public removeCommand(eventScope: PluginEvent): void
public removeCommand(...args: any): void {
if ('command' in args[0]) {
let commandInfo: CommandInfo = args[0];
this.commandInfoList = this.commandInfoList.filter((commandInfoItem) => commandInfoItem.commandInfo !== commandInfo);
delete this.commandList[commandInfo.command.toLocaleLowerCase()];
if (Array.isArray(commandInfo.alias)) {
commandInfo.alias.forEach((alias) => {
delete this.commandList[alias.toLocaleLowerCase()];
});
}
} else if (typeof args[0] !== 'undefined' && args[0].pluginId) {
let eventScope: PluginEvent = args[0];
this.commandInfoList = this.commandInfoList.filter((commandInfoItem) => commandInfoItem.eventScope !== eventScope);
for (let command in this.commandList) {
if (this.commandList[command].eventScope.pluginId === eventScope.pluginId) {
delete this.commandList[command];
}
}
}
}
public async emit(eventName: string, senderInfo?: ChatIdentity | null, ...args: any[]) {
if (this.app.debug) {
if (typeof args[0] === 'object' && args[0].chatType) {
this.app.logger.debug(`触发事件 ${eventName} ${args[0].contentText}`);
} else {
this.app.logger.debug(`触发事件 ${eventName}`);
}
}
const eventList = this.eventList[eventName];
if (!eventList) return false;
const isFilter = eventName.startsWith('filter/');
let isResolved = false;
const resolved = () => {
isResolved = true;
};
const buildOnError = (eventInfo: ControllerEventInfo) => (error: Error) => {
this.app.logger.error(`[${eventInfo.eventScope.pluginId}/${eventInfo.eventScope.scopeName}] 处理事件 ${eventName} 时出错`, error);
console.error(error);
for (let arg of args) {
if (typeof arg === 'object' && arg.chatType) {
const msg: CommonReceivedMessage = arg;
if (error instanceof RateLimitError) {
const retryAfterMinutes = Math.ceil(error.retryAfter / 60);
msg.sendReply(`使用太多了,${retryAfterMinutes}分钟后再试吧`);
} else if (error instanceof PermissionDeniedError) {
msg.sendReply(`使用此功能需要${error.requiredPermission}权限`);
}
break;
}
}
}
let [subscribedPlugins, disabledPlugins] = this.getPluginSubscribe(senderInfo);
for (let eventInfo of eventList) {
if (!isFilter && senderInfo) {
switch (senderInfo.type) {
case 'private':
if (!eventInfo.eventScope.allowPrivate) {
continue;
}
if (!eventInfo.eventScope.isAllowSubscribe(senderInfo)) {
continue;
}
break;
case 'group':
if (!eventInfo.eventScope.allowGroup) {
continue;
}
break;
case 'channel':
if (!eventInfo.eventScope.allowChannel) {
continue;
}
break;
}
if (senderInfo.type !== 'private') { // 私聊消息不存在订阅,只判断群消息和频道消息
const eventScope = eventInfo.eventScope;
if (eventScope.autoSubscribe) {
if (!eventScope.isAllowSubscribe(senderInfo)) {
continue;
} else {
// 检测控制器是否已禁用
if (this.isPluginScopeInList(eventScope.pluginId, eventScope.scopeName, disabledPlugins)) {
continue;
}
}
} else {
// 检测控制器是否已启用
if (!this.isPluginScopeInList(eventScope.pluginId, eventScope.scopeName, subscribedPlugins)) {
continue;
}
}
}
}
try {
const ret = await eventInfo.callback(...args, resolved);
if (isResolved) {
break;
}
// detect ret is promise
if (ret && typeof ret.catch === 'function') {
ret.catch(buildOnError(eventInfo));
}
} catch(err: any) {
buildOnError(eventInfo)(err);
}
}
return isResolved;
}
public async emitMessage(message: Reactive<CommonReceivedMessage>) {
let isResolved = false;
if (message.chatType === 'private' || (message.chatType === 'group' && message.mentionedReceiver)) {
if (this.app.config.focused_as_command) {
isResolved = await this.emitCommand(message.contentText, message);
if (isResolved) return true;
}
isResolved = await this.emit(`message/focused`, this.getSenderInfo(message), message);
if (isResolved) return true;
}
isResolved = await this.emit(`message/${message.chatType}`, this.getSenderInfo(message), message);
if (isResolved) return true;
isResolved = await this.emit('message', this.getSenderInfo(message), message);
if (isResolved) return true;
return false;
}
public async emitCommand(contentText: string, message: Reactive<CommonReceivedMessage>) {
let command = '';
let param = '';
// 尝试识别空格分隔的指令
if (contentText.includes(' ')) {
command = contentText.split(' ')[0].toLocaleLowerCase();
param = contentText.substring(command.length + 1);
if (!(command in this.commandList)) {
command = '';
}
}
// 尝试使用最长匹配查找指令
if (command.length === 0) {
for (let registeredCommand in this.commandList) {
if (contentText.startsWith(registeredCommand)) {
if (registeredCommand.length > command.length) {
command = registeredCommand;
}
}
}
if (command.length === 0) {
return false;
}
param = contentText.substring(command.length);
}
if (this.app.debug) {
this.app.logger.debug('指令识别结果', command, param);
}
let commandArgs: CommandInputArgs = {
command,
param
};
return await this.emit(`command/${command}`, this.getSenderInfo(message), commandArgs, message);
}
public async emitRawEvent(robot: Robot, event: string, ...args: any[]) {
return await this.emit(`raw/${robot.type}/${event}`, { type: 'raw', robot: robot }, event);
}
public async emitRawMessage(message: Reactive<CommonReceivedMessage>) {
let isResolved = false;
await this.emit(`filter/message`, null, message);
isResolved = await this.emit(`raw/${message.receiver.type}/message`, this.getSenderInfo(message), message);
if (isResolved) return true;
return await this.emit('raw/message', this.getSenderInfo(message), message);
}
public async emitFilterSendMessage(message: Reactive<CommonReceivedMessage>) {
}
public getSenderInfo(message: CommonReceivedMessage): ChatIdentity {
if (message.chatType === 'private') {
return {
type: 'private',
robot: message.receiver,
userId: message.sender.userId
};
} else if (message.chatType === 'group') {
return {
type: 'group',
robot: message.receiver,
groupId: message.sender.groupId,
userId: message.sender.userId
};
}
return {
type: 'unknown',
robot: message.receiver
}
}
public getPluginSubscribe(senderInfo?: ChatIdentity | null): [SubscribeItem[], SubscribeItem[]] {
let subscribedCommands: SubscribeItem[] = [];
let disabledCommands: SubscribeItem[] = [];
if (senderInfo) {
let targetType = '';
let targetId = '';
switch (senderInfo.type) {
case 'private':
targetType = 'user';
targetId = senderInfo.userId!;
break;
case 'group':
targetType = 'group';
targetId = senderInfo.groupId!;
break;
case 'channel':
targetType = 'channel';
targetId = senderInfo.channelId!;
break;
}
subscribedCommands = this.app.subscribe.getSubscribedList(senderInfo.robot.robotId!, targetType, targetId, 'controller');
disabledCommands = this.app.subscribe.getSubscribedList(senderInfo.robot.robotId!, targetType, targetId, 'disable_controller');
}
return [
subscribedCommands,
disabledCommands
];
}
public isPluginScopeInList(pluginId: string, scopeName: string, scopeList: SubscribeItem[]): boolean {
return scopeList.some((scope) =>
scope[0] === pluginId && (scope[1] === "*" || scope[1] === scopeName)
);
}
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);
}
}