|
|
|
@ -1,21 +1,27 @@
|
|
|
|
|
import { EventManager } from "./EventManager";
|
|
|
|
|
import { CommonReceivedMessage } from "./message/Message";
|
|
|
|
|
import { Robot } from "./RobotManager";
|
|
|
|
|
import fs from 'fs';
|
|
|
|
|
import fsAsync from 'fs/promises';
|
|
|
|
|
import chokidar from 'chokidar';
|
|
|
|
|
import Yaml from 'yaml';
|
|
|
|
|
import App from "./App";
|
|
|
|
|
import EventEmitter from "events";
|
|
|
|
|
import path from "path";
|
|
|
|
|
import { SenderIdentity } from "./message/Sender";
|
|
|
|
|
import { Utils } from "./utils/Utils";
|
|
|
|
|
|
|
|
|
|
export const MessagePriority = {
|
|
|
|
|
LOWEST: 0,
|
|
|
|
|
LOW: 20,
|
|
|
|
|
DEFAULT: 40,
|
|
|
|
|
HIGH: 50,
|
|
|
|
|
/**
|
|
|
|
|
* 在控制器中添加的临时会话处理器。
|
|
|
|
|
* 用于处理深层会话,会比一般指令优先级高。
|
|
|
|
|
* 在控制器中添加的会话处理器。
|
|
|
|
|
* 用于处理独占模式会话,会比一般指令优先级高,且会阻止所有后续事件。
|
|
|
|
|
*/
|
|
|
|
|
TEMP_HANDLER: 60,
|
|
|
|
|
HIGH: 80,
|
|
|
|
|
SESSION_HANDLER: 60,
|
|
|
|
|
HIGHER: 70,
|
|
|
|
|
/**
|
|
|
|
|
* 一些系统自带的事件处理,如:记录消息
|
|
|
|
|
*/
|
|
|
|
@ -48,29 +54,26 @@ export type RawEventCallback = (robot: Robot, event: any, resolved: VoidFunction
|
|
|
|
|
|
|
|
|
|
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 configPath: string;
|
|
|
|
|
|
|
|
|
|
private watcher!: chokidar.FSWatcher;
|
|
|
|
|
private configWatcher!: chokidar.FSWatcher;
|
|
|
|
|
public controllers: Record<string, PluginController>;
|
|
|
|
|
public fileControllers: Record<string, PluginController>;
|
|
|
|
|
public configControllers: Record<string, PluginController>;
|
|
|
|
|
|
|
|
|
|
constructor(app: App, pluginPath: string) {
|
|
|
|
|
constructor(app: App, pluginPath: string, configPath: string) {
|
|
|
|
|
super();
|
|
|
|
|
|
|
|
|
|
this.app = app;
|
|
|
|
|
this.pluginPath = pluginPath;
|
|
|
|
|
this.pluginPath = path.resolve(pluginPath);
|
|
|
|
|
this.configPath = path.resolve(configPath);
|
|
|
|
|
this.controllers = {};
|
|
|
|
|
this.fileControllers = {};
|
|
|
|
|
this.configControllers = {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -82,10 +85,15 @@ export class PluginManager extends EventEmitter {
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
this.configWatcher = chokidar.watch(this.configPath + '/**/*.yml', {
|
|
|
|
|
ignorePermissionErrors: true,
|
|
|
|
|
persistent: true
|
|
|
|
|
});
|
|
|
|
|
this.configWatcher.on('change', this.reloadConfig.bind(this));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async loadController(file: string) {
|
|
|
|
@ -111,16 +119,19 @@ export class PluginManager extends EventEmitter {
|
|
|
|
|
this.fileControllers[file] = controllerInstance;
|
|
|
|
|
|
|
|
|
|
if (isReload) {
|
|
|
|
|
console.log(`已重新加载Controller: ${file}`);
|
|
|
|
|
this.app.logger.info(`已重新加载Controller: ${file}`);
|
|
|
|
|
this.emit('controllerReloaded', controllerInstance);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`已加载Controller: ${file}`);
|
|
|
|
|
this.app.logger.info(`已加载Controller: ${file}`);
|
|
|
|
|
this.emit('controllerLoaded', controllerInstance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pluginMisc = new PluginEvent(this.app.event);
|
|
|
|
|
controllerInstance.event = pluginMisc;
|
|
|
|
|
await controllerInstance.initialize();
|
|
|
|
|
const pluginEvent = new PluginEvent(this.app);
|
|
|
|
|
controllerInstance.event = pluginEvent;
|
|
|
|
|
|
|
|
|
|
const controllerConfig = await this.loadControllerConfig('standalone', controllerInstance);
|
|
|
|
|
|
|
|
|
|
await controllerInstance.initialize(controllerConfig);
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('PluginController ID is not defined.');
|
|
|
|
|
}
|
|
|
|
@ -136,76 +147,174 @@ export class PluginManager extends EventEmitter {
|
|
|
|
|
async removeController(file: string, isReload = false) {
|
|
|
|
|
const controller = this.fileControllers[file];
|
|
|
|
|
if (controller) {
|
|
|
|
|
const configFile = this.getConfigFile('standalone', controller);
|
|
|
|
|
|
|
|
|
|
await controller.event.destroy();
|
|
|
|
|
await controller.destroy?.();
|
|
|
|
|
|
|
|
|
|
delete this.controllers[file];
|
|
|
|
|
delete this.fileControllers[file];
|
|
|
|
|
if (configFile in this.configControllers) {
|
|
|
|
|
delete this.configControllers[configFile];
|
|
|
|
|
}
|
|
|
|
|
this.emit('controllerRemoved', controller);
|
|
|
|
|
|
|
|
|
|
if (!isReload) {
|
|
|
|
|
console.log(`已移除Controller: ${controller.id}`);
|
|
|
|
|
this.app.logger.info(`已移除Controller: ${controller.id}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getConfigFile(pluginId: string, controller: PluginController) {
|
|
|
|
|
return path.resolve(this.configPath, pluginId, controller.id + '.yml');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface PluginController {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
async loadControllerConfig(pluginId: string, controller: PluginController) {
|
|
|
|
|
const configFile = this.getConfigFile(pluginId, controller);
|
|
|
|
|
try {
|
|
|
|
|
if (configFile in this.configControllers) { // 防止保存时触发重载
|
|
|
|
|
delete this.configControllers[configFile];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
event: PluginEvent;
|
|
|
|
|
const defaultConfig = await controller.getDefaultConfig?.() ?? {};
|
|
|
|
|
let config: any = defaultConfig;
|
|
|
|
|
let shouldFill: boolean = false;
|
|
|
|
|
|
|
|
|
|
initialize: () => Promise<void>;
|
|
|
|
|
destroy?: () => Promise<void>;
|
|
|
|
|
if (fs.existsSync(configFile)) {
|
|
|
|
|
let localConfig = Yaml.parse(await fsAsync.readFile(configFile, 'utf-8'));
|
|
|
|
|
config = {...defaultConfig, ...localConfig};
|
|
|
|
|
if (!Utils.compare(config, localConfig)) {
|
|
|
|
|
shouldFill = true;
|
|
|
|
|
this.app.logger.info(`配置文件已生成: ${configFile}`);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
shouldFill = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class PluginEvent {
|
|
|
|
|
private eventManager: EventManager;
|
|
|
|
|
|
|
|
|
|
public controller?: PluginController;
|
|
|
|
|
if (shouldFill) {
|
|
|
|
|
Utils.prepareDir(path.dirname(configFile));
|
|
|
|
|
await fsAsync.writeFile(configFile, Yaml.stringify(config));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public autoSubscribe = false;
|
|
|
|
|
public forceSubscribe = false;
|
|
|
|
|
public showInSubscribeList = true;
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.configControllers[configFile] = controller;
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
|
|
|
|
public allowPrivate = true;
|
|
|
|
|
public allowGroup = true;
|
|
|
|
|
public allowChannel = true;
|
|
|
|
|
return config;
|
|
|
|
|
} catch(err: any) {
|
|
|
|
|
this.app.logger.error(`加载Controller配置失败: ${configFile}`, err);
|
|
|
|
|
console.error(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public allowedRobotTypeList: AllowedList = '*';
|
|
|
|
|
async reloadConfig(file: string) {
|
|
|
|
|
this.app.logger.info(`配置文件已更新: ${file}`);
|
|
|
|
|
if (file in this.configControllers) {
|
|
|
|
|
try {
|
|
|
|
|
const controller = this.configControllers[file];
|
|
|
|
|
if (controller.updateConfig) { // 如果控制器支持重载配置,则直接调用
|
|
|
|
|
const localConfig = Yaml.parse(await fsAsync.readFile(file, 'utf-8'));
|
|
|
|
|
await controller.updateConfig(localConfig);
|
|
|
|
|
this.app.logger.info(`已重载Controller配置: ${controller.id}`);
|
|
|
|
|
} else { // 重载整个控制器
|
|
|
|
|
let controllerFile: string = '';
|
|
|
|
|
for (let [file, c] of Object.entries(this.fileControllers)) {
|
|
|
|
|
if (c === controller) {
|
|
|
|
|
controllerFile = file;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (controllerFile) {
|
|
|
|
|
await this.loadController(controllerFile);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch(err: any) {
|
|
|
|
|
this.app.logger.error(`重载Controller配置失败: ${file}`, err);
|
|
|
|
|
console.error(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private commandList: CommandInfo[] = [];
|
|
|
|
|
private eventList: Record<string, EventListenerInfo[]> = {};
|
|
|
|
|
/**
|
|
|
|
|
* 获取订阅的控制器
|
|
|
|
|
* @param senderInfo
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
public getSubscribedControllers(senderInfo: SenderIdentity): PluginController[] {
|
|
|
|
|
let [subscribedControllers, disabledControllers] = this.app.event.getControllerSubscribe(senderInfo);
|
|
|
|
|
|
|
|
|
|
constructor(eventManager: EventManager) {
|
|
|
|
|
this.eventManager = eventManager;
|
|
|
|
|
}
|
|
|
|
|
return Object.values(this.controllers).filter((controller) => {
|
|
|
|
|
if (controller.event.commandList.length === 0) return false;
|
|
|
|
|
|
|
|
|
|
public isAllowSubscribe: (source: ControllerSubscribeSource) => boolean = (source) => {
|
|
|
|
|
if (this.allowedRobotTypeList !== '*' && !this.allowedRobotTypeList.includes(source.robot.type)) {
|
|
|
|
|
switch (senderInfo.type) {
|
|
|
|
|
case 'private':
|
|
|
|
|
if (!controller.event.allowPrivate) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (source.type) {
|
|
|
|
|
case 'private':
|
|
|
|
|
if (!this.allowPrivate) {
|
|
|
|
|
if (!controller.event.isAllowSubscribe(senderInfo)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'group':
|
|
|
|
|
if (!this.allowGroup) {
|
|
|
|
|
if (!controller.event.allowGroup) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'channel':
|
|
|
|
|
if (!this.allowChannel) {
|
|
|
|
|
if (!controller.event.allowChannel) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (senderInfo.type !== 'private') { // 私聊消息不存在订阅,只判断群消息和频道消息
|
|
|
|
|
if (controller.event.autoSubscribe) {
|
|
|
|
|
if (!controller.event.isAllowSubscribe(senderInfo)) {
|
|
|
|
|
return false;
|
|
|
|
|
} else {
|
|
|
|
|
// 检测控制器是否已禁用
|
|
|
|
|
if (disabledControllers.includes(controller.id)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 检测控制器是否已启用
|
|
|
|
|
if (!subscribedControllers.includes(controller.id)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface PluginController {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
|
|
|
|
|
event: PluginEvent;
|
|
|
|
|
|
|
|
|
|
initialize: (config: any) => Promise<void>;
|
|
|
|
|
destroy?: () => Promise<void>;
|
|
|
|
|
|
|
|
|
|
getDefaultConfig?: () => Promise<any>;
|
|
|
|
|
updateConfig?: (config: any) => Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class EventScope {
|
|
|
|
|
protected app: App;
|
|
|
|
|
protected eventManager: EventManager;
|
|
|
|
|
|
|
|
|
|
public commandList: CommandInfo[] = [];
|
|
|
|
|
public eventList: Record<string, EventListenerInfo[]> = {};
|
|
|
|
|
public eventSorted: Record<string, boolean> = {};
|
|
|
|
|
|
|
|
|
|
constructor(app: App) {
|
|
|
|
|
this.app = app;
|
|
|
|
|
this.eventManager = app.event;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -295,8 +404,9 @@ export class PluginEvent {
|
|
|
|
|
priority: options.priority!
|
|
|
|
|
};
|
|
|
|
|
this.eventList[event].push(eventInfo);
|
|
|
|
|
this.eventSorted[event] = false;
|
|
|
|
|
|
|
|
|
|
this.eventManager.on(event, this, callback, options);
|
|
|
|
|
this.afterAddEventListener(event, callback, options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public off(event: string, callback: CallableFunction): void {
|
|
|
|
@ -306,17 +416,58 @@ export class PluginEvent {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.eventManager.off(event, this, callback);
|
|
|
|
|
this.afterRemoveEventListener(event, callback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Trigger event.
|
|
|
|
|
* @param event Event name
|
|
|
|
|
* @param args Arguments
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
public async emit(event: string, ...args: any[]) {
|
|
|
|
|
let isResolved = false;
|
|
|
|
|
|
|
|
|
|
const resolved = () => {
|
|
|
|
|
isResolved = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (event in this.eventList) {
|
|
|
|
|
if (!this.eventSorted[event]) { // 如果事件未排序,触发排序
|
|
|
|
|
this.eventList[event].sort((a, b) => {
|
|
|
|
|
return a.priority - b.priority;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const eventInfo of this.eventList[event]) {
|
|
|
|
|
try {
|
|
|
|
|
await eventInfo.callback(...args, resolved);
|
|
|
|
|
if (isResolved) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
this.app.logger.error(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return isResolved;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Register command.
|
|
|
|
|
* @param command
|
|
|
|
|
* @param name
|
|
|
|
|
* @param callback
|
|
|
|
|
* @param options
|
|
|
|
|
* @param command Command
|
|
|
|
|
* @param name Command name
|
|
|
|
|
* @param callback Callback function
|
|
|
|
|
* @param options Command options
|
|
|
|
|
*/
|
|
|
|
|
public registerCommand(command: string, name: string, callback: CommandCallback, options?: MessageEventOptions): void
|
|
|
|
|
/**
|
|
|
|
|
* Register command.
|
|
|
|
|
* @param commandInfo Command info
|
|
|
|
|
* @param callback Callback function
|
|
|
|
|
* @param options Command options
|
|
|
|
|
*/
|
|
|
|
|
public registerCommand(commandInfo: CommandInfo, callback: CommandCallback, options?: MessageEventOptions): void
|
|
|
|
|
public registerCommand(...args: any[]): void {
|
|
|
|
|
// 处理传入参数
|
|
|
|
@ -341,20 +492,64 @@ export class PluginEvent {
|
|
|
|
|
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);
|
|
|
|
|
this.on(`command/${cmd.toLocaleLowerCase()}`, callback, options);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.eventManager.addCommand(commandInfo as any, this);
|
|
|
|
|
this.afterAddCommand(commandInfo as any);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize plugin controller.
|
|
|
|
|
*/
|
|
|
|
|
public async initialize() {
|
|
|
|
|
protected afterAddEventListener(event: string, callback: CallableFunction, options?: MessageEventOptions): void {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected afterRemoveEventListener(event: string, callback: CallableFunction): void { }
|
|
|
|
|
|
|
|
|
|
protected afterAddCommand(commandInfo: CommandInfo): void { }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class PluginEvent extends EventScope {
|
|
|
|
|
public controller?: PluginController;
|
|
|
|
|
|
|
|
|
|
public autoSubscribe = false;
|
|
|
|
|
public forceSubscribe = false;
|
|
|
|
|
public showInSubscribeList = true;
|
|
|
|
|
|
|
|
|
|
public allowPrivate = true;
|
|
|
|
|
public allowGroup = true;
|
|
|
|
|
public allowChannel = true;
|
|
|
|
|
|
|
|
|
|
public allowedRobotTypeList: AllowedList = '*';
|
|
|
|
|
|
|
|
|
|
public isAllowSubscribe: (source: SenderIdentity) => 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public init(controller: PluginController) {
|
|
|
|
|
this.controller = controller;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Destroy eventGroup.
|
|
|
|
|
* Will remove all event listeners.
|
|
|
|
@ -365,4 +560,16 @@ export class PluginEvent {
|
|
|
|
|
|
|
|
|
|
this.eventList = {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override afterAddEventListener(event: string, callback: CallableFunction, options?: MessageEventOptions): void {
|
|
|
|
|
this.eventManager.on(event, this, callback, options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override afterRemoveEventListener(event: string, callback: CallableFunction): void {
|
|
|
|
|
this.eventManager.off(event, this, callback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override afterAddCommand(commandInfo: CommandInfo): void {
|
|
|
|
|
this.eventManager.addCommand(commandInfo as any, this);
|
|
|
|
|
}
|
|
|
|
|
}
|