|
|
|
@ -11,6 +11,8 @@ import { ChatIdentity } from "./message/Sender";
|
|
|
|
|
import { Utils } from "./utils/Utils";
|
|
|
|
|
import { Robot } from "./robot/Robot";
|
|
|
|
|
import { Reactive } from "./utils/reactive";
|
|
|
|
|
import { PluginController } from "#ibot-api/PluginController";
|
|
|
|
|
import { PluginApiBridge } from "./plugin/PluginApiBridge";
|
|
|
|
|
|
|
|
|
|
export const MessagePriority = {
|
|
|
|
|
LOWEST: 0,
|
|
|
|
@ -60,6 +62,19 @@ export type RawEventCallback = (robot: Robot, event: any, resolved: VoidFunction
|
|
|
|
|
|
|
|
|
|
export type AllowedList = string[] | '*';
|
|
|
|
|
|
|
|
|
|
export type SubscribedPluginInfo = {
|
|
|
|
|
id: string,
|
|
|
|
|
controller: PluginController,
|
|
|
|
|
eventGroups: PluginEvent[],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type PluginInstance = {
|
|
|
|
|
id: string,
|
|
|
|
|
path: string,
|
|
|
|
|
bridge: PluginApiBridge,
|
|
|
|
|
controller: PluginController,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class PluginManager extends EventEmitter {
|
|
|
|
|
private app: App;
|
|
|
|
|
private pluginPath: string;
|
|
|
|
@ -67,9 +82,9 @@ export class PluginManager extends EventEmitter {
|
|
|
|
|
|
|
|
|
|
private watcher!: chokidar.FSWatcher;
|
|
|
|
|
private configWatcher!: chokidar.FSWatcher;
|
|
|
|
|
public controllers: Record<string, PluginController>;
|
|
|
|
|
public fileControllers: Record<string, PluginController>;
|
|
|
|
|
public configControllers: Record<string, PluginController>;
|
|
|
|
|
|
|
|
|
|
public pluginInstanceMap: Record<string, PluginInstance> = {};
|
|
|
|
|
public configPluginMap: Record<string, string> = {};
|
|
|
|
|
|
|
|
|
|
constructor(app: App, pluginPath: string, configPath: string) {
|
|
|
|
|
super();
|
|
|
|
@ -77,67 +92,102 @@ export class PluginManager extends EventEmitter {
|
|
|
|
|
this.app = app;
|
|
|
|
|
this.pluginPath = path.resolve(pluginPath);
|
|
|
|
|
this.configPath = path.resolve(configPath);
|
|
|
|
|
this.controllers = {};
|
|
|
|
|
this.fileControllers = {};
|
|
|
|
|
this.configControllers = {};
|
|
|
|
|
this.pluginInstanceMap = {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 加载所有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));
|
|
|
|
|
// this.watcher = chokidar.watch(this.pluginPath + "/**/*.js", {
|
|
|
|
|
// ignorePermissionErrors: true,
|
|
|
|
|
// persistent: true,
|
|
|
|
|
// followSymlinks: true,
|
|
|
|
|
// depth: 1
|
|
|
|
|
// });
|
|
|
|
|
// this.watcher.on('add', this.onPluginFileAdded.bind(this));
|
|
|
|
|
// this.watcher.on('change', this.onPluginFileChanged.bind(this));
|
|
|
|
|
// this.watcher.on('unlink', this.onPluginFileRemoved.bind(this));
|
|
|
|
|
|
|
|
|
|
this.configWatcher = chokidar.watch(this.configPath + '/**/*.yml', {
|
|
|
|
|
for (let folder of fs.readdirSync(this.pluginPath)) {
|
|
|
|
|
if (folder.startsWith('.')) continue;
|
|
|
|
|
|
|
|
|
|
let pluginPath = path.join(this.pluginPath, folder);
|
|
|
|
|
if (!fs.statSync(pluginPath).isDirectory()) continue;
|
|
|
|
|
|
|
|
|
|
await this.loadPlugin(pluginPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.configWatcher = chokidar.watch(this.configPath + '/plugin/*.yaml', {
|
|
|
|
|
ignorePermissionErrors: true,
|
|
|
|
|
persistent: true
|
|
|
|
|
});
|
|
|
|
|
this.configWatcher.on('change', this.reloadConfig.bind(this));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async loadController(file: string) {
|
|
|
|
|
if (!file.match(/Controller\.m?js$/)) return;
|
|
|
|
|
|
|
|
|
|
let moduleName = path.resolve(file).replace(/\\/g, '/').replace(/\.m?js$/, '');
|
|
|
|
|
async loadPlugin(folder: string) {
|
|
|
|
|
folder = path.resolve(folder);
|
|
|
|
|
this.app.logger.debug('尝试从 ' + folder + ' 加载插件');
|
|
|
|
|
const pluginIndexFile = path.join(folder, 'plugin.yaml');
|
|
|
|
|
if (!fs.existsSync(pluginIndexFile)) return;
|
|
|
|
|
|
|
|
|
|
let pluginId = '';
|
|
|
|
|
try {
|
|
|
|
|
const controller = await import(moduleName);
|
|
|
|
|
const pluginIndex = Yaml.parse(await fsAsync.readFile(pluginIndexFile, 'utf-8'));
|
|
|
|
|
|
|
|
|
|
if (!pluginIndex || typeof pluginIndex.controller !== "string") {
|
|
|
|
|
this.app.logger.error('插件 ' + folder + ' 没有指定主文件');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!pluginIndex.controller.endsWith('.js')) {
|
|
|
|
|
pluginIndex.controller += '.js';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const controllerFile = path.join(folder, pluginIndex.controller);
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(controllerFile)) {
|
|
|
|
|
this.app.logger.error('插件 ' + folder + ' 控制器 ' + controllerFile + ' 不存在');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const controller = await import(controllerFile);
|
|
|
|
|
if (controller) {
|
|
|
|
|
const controllerClass = controller.default ?? controller;
|
|
|
|
|
const controllerInstance: PluginController = new controllerClass(this.app);
|
|
|
|
|
if (controllerInstance.id && controllerInstance.id !== '') {
|
|
|
|
|
const controllerId = controllerInstance.id;
|
|
|
|
|
const controllerClass: typeof PluginController = controller.default ?? controller;
|
|
|
|
|
if (controllerClass.id) {
|
|
|
|
|
pluginId = controllerClass.id;
|
|
|
|
|
|
|
|
|
|
const pluginApiBridge = new PluginApiBridge(this.app, pluginId);
|
|
|
|
|
const controllerInstance: PluginController = new controllerClass(this.app, pluginApiBridge);
|
|
|
|
|
|
|
|
|
|
const pluginInstance: PluginInstance = {
|
|
|
|
|
id: pluginId,
|
|
|
|
|
path: folder,
|
|
|
|
|
bridge: pluginApiBridge,
|
|
|
|
|
controller: controllerInstance
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pluginApiBridge.setController(controllerInstance);
|
|
|
|
|
|
|
|
|
|
let isReload = false;
|
|
|
|
|
if (controllerId in this.controllers) {
|
|
|
|
|
if (pluginId in this.pluginInstanceMap) {
|
|
|
|
|
// Reload plugin
|
|
|
|
|
isReload = true;
|
|
|
|
|
await this.removeController(file, true);
|
|
|
|
|
await this.unloadPlugin(pluginId, true);
|
|
|
|
|
}
|
|
|
|
|
this.controllers[controllerId] = controllerInstance;
|
|
|
|
|
this.fileControllers[file] = controllerInstance;
|
|
|
|
|
|
|
|
|
|
this.pluginInstanceMap[pluginId] = pluginInstance;
|
|
|
|
|
|
|
|
|
|
if (isReload) {
|
|
|
|
|
this.app.logger.info(`已重新加载Controller: ${file}`);
|
|
|
|
|
this.emit('controllerReloaded', controllerInstance);
|
|
|
|
|
this.app.logger.info(`已重新加载插件: ${pluginId}`);
|
|
|
|
|
this.emit('pluginReloaded', controllerInstance);
|
|
|
|
|
} else {
|
|
|
|
|
this.app.logger.info(`已加载Controller: ${file}`);
|
|
|
|
|
this.emit('controllerLoaded', controllerInstance);
|
|
|
|
|
this.app.logger.info(`已加载插件: ${pluginId}`);
|
|
|
|
|
this.emit('pluginLoaded', controllerInstance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pluginEvent = new PluginEvent(this.app);
|
|
|
|
|
controllerInstance.event = pluginEvent;
|
|
|
|
|
|
|
|
|
|
const controllerConfig = await this.loadControllerConfig('standalone', controllerInstance);
|
|
|
|
|
const controllerConfig = await this.loadMainConfig(pluginId, controllerInstance);
|
|
|
|
|
|
|
|
|
|
await controllerInstance.initialize(controllerConfig);
|
|
|
|
|
await controllerInstance._initialize(controllerConfig);
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('PluginController ID is not defined.');
|
|
|
|
|
}
|
|
|
|
@ -145,41 +195,64 @@ export class PluginManager extends EventEmitter {
|
|
|
|
|
throw new Error('PluginController does not have an export.');
|
|
|
|
|
}
|
|
|
|
|
} catch(err: any) {
|
|
|
|
|
console.error(`加载Controller失败: ${file}`);
|
|
|
|
|
console.error(`加载插件失败: ${folder}`);
|
|
|
|
|
console.error(err);
|
|
|
|
|
|
|
|
|
|
if (pluginId && this.pluginInstanceMap[pluginId]) {
|
|
|
|
|
delete this.pluginInstanceMap[pluginId];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async removeController(file: string, isReload = false) {
|
|
|
|
|
const controller = this.fileControllers[file];
|
|
|
|
|
if (controller) {
|
|
|
|
|
const configFile = this.getConfigFile('standalone', controller);
|
|
|
|
|
async unloadPlugin(pluginId: string, isReload = false) {
|
|
|
|
|
const instance = this.pluginInstanceMap[pluginId];
|
|
|
|
|
if (instance) {
|
|
|
|
|
const configFile = this.getConfigFile(pluginId);
|
|
|
|
|
|
|
|
|
|
await instance.bridge.destroy();
|
|
|
|
|
await instance.controller.destroy?.();
|
|
|
|
|
|
|
|
|
|
await controller.event.destroy();
|
|
|
|
|
await controller.destroy?.();
|
|
|
|
|
delete this.pluginInstanceMap[pluginId];
|
|
|
|
|
|
|
|
|
|
delete this.controllers[file];
|
|
|
|
|
delete this.fileControllers[file];
|
|
|
|
|
if (configFile in this.configControllers) {
|
|
|
|
|
delete this.configControllers[configFile];
|
|
|
|
|
if (configFile in this.configPluginMap) {
|
|
|
|
|
delete this.configPluginMap[configFile];
|
|
|
|
|
}
|
|
|
|
|
this.emit('controllerRemoved', controller);
|
|
|
|
|
this.emit('pluginUnloaded', instance);
|
|
|
|
|
|
|
|
|
|
if (!isReload) {
|
|
|
|
|
this.app.logger.info(`已移除Controller: ${controller.id}`);
|
|
|
|
|
this.app.logger.info(`已关闭插件: ${pluginId}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getConfigFile(pluginId: string, controller: PluginController) {
|
|
|
|
|
return path.resolve(this.configPath, pluginId, controller.id + '.yml');
|
|
|
|
|
async reloadPlugin(pluginId: string) {
|
|
|
|
|
let pluginInstance = this.pluginInstanceMap[pluginId];
|
|
|
|
|
if (!pluginInstance) return;
|
|
|
|
|
|
|
|
|
|
await this.loadPlugin(pluginInstance.path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async loadControllerConfig(pluginId: string, controller: PluginController) {
|
|
|
|
|
const configFile = this.getConfigFile(pluginId, controller);
|
|
|
|
|
getPluginPathFromFile(filePath: string) {
|
|
|
|
|
if (filePath.startsWith(this.pluginPath)) {
|
|
|
|
|
return filePath.substring(this.pluginPath.length + 1).split(path.sep)[0];
|
|
|
|
|
} else {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onPluginFileChanged(filePath: string) {
|
|
|
|
|
// Unfinished
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getConfigFile(pluginId: string) {
|
|
|
|
|
return path.resolve(this.configPath, "plugin", pluginId + '.yaml');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async loadMainConfig(pluginId: string, controller: PluginController) {
|
|
|
|
|
const configFile = this.getConfigFile(pluginId);
|
|
|
|
|
try {
|
|
|
|
|
if (configFile in this.configControllers) { // 防止保存时触发重载
|
|
|
|
|
delete this.configControllers[configFile];
|
|
|
|
|
if (configFile in this.configPluginMap) { // 防止保存时触发重载
|
|
|
|
|
delete this.configPluginMap[configFile];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultConfig = await controller.getDefaultConfig?.() ?? {};
|
|
|
|
@ -203,124 +276,128 @@ export class PluginManager extends EventEmitter {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.configControllers[configFile] = controller;
|
|
|
|
|
this.configPluginMap[configFile] = pluginId;
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
} catch(err: any) {
|
|
|
|
|
this.app.logger.error(`加载Controller配置失败: ${configFile}`, err);
|
|
|
|
|
this.app.logger.error(`加载插件主配置文件失败: ${configFile}`, err);
|
|
|
|
|
console.error(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async reloadConfig(file: string) {
|
|
|
|
|
this.app.logger.info(`配置文件已更新: ${file}`);
|
|
|
|
|
if (file in this.configControllers) {
|
|
|
|
|
if (file in this.configPluginMap) {
|
|
|
|
|
const pluginId = this.configPluginMap[file];
|
|
|
|
|
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);
|
|
|
|
|
const pluginInstance = this.pluginInstanceMap[pluginId];
|
|
|
|
|
if (pluginInstance) {
|
|
|
|
|
const ctor = pluginInstance.controller.constructor as typeof PluginController;
|
|
|
|
|
if (ctor.reloadWhenConfigUpdated) { // 重载整个控制器
|
|
|
|
|
await this.reloadPlugin(pluginId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const localConfig = Yaml.parse(await fsAsync.readFile(file, 'utf-8'));
|
|
|
|
|
await pluginInstance.controller._setConfig(localConfig);
|
|
|
|
|
this.app.logger.info(`已重载插件配置文件: ${pluginId}`);
|
|
|
|
|
}
|
|
|
|
|
} catch(err: any) {
|
|
|
|
|
this.app.logger.error(`重载Controller配置失败: ${file}`, err);
|
|
|
|
|
this.app.logger.error(`重载插件 [${pluginId}] 配置失败: ${file}`, err);
|
|
|
|
|
console.error(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取订阅的控制器
|
|
|
|
|
* 获取订阅的控制器和事件组
|
|
|
|
|
* @param senderInfo
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
public getSubscribedControllers(senderInfo: ChatIdentity): PluginController[] {
|
|
|
|
|
let [subscribedControllers, disabledControllers] = this.app.event.getControllerSubscribe(senderInfo);
|
|
|
|
|
public getSubscribed(senderInfo: ChatIdentity): SubscribedPluginInfo[] {
|
|
|
|
|
let [subscribedScopes, disabledScopes] = this.app.event.getPluginSubscribe(senderInfo);
|
|
|
|
|
|
|
|
|
|
let subscribed: SubscribedPluginInfo[] = [];
|
|
|
|
|
for (let pluginInstance of Object.values(this.pluginInstanceMap)) {
|
|
|
|
|
let eventGroups: PluginEvent[] = [];
|
|
|
|
|
for (let scopeName in pluginInstance.bridge.scopedEvent) {
|
|
|
|
|
let eventGroup = pluginInstance.bridge.scopedEvent[scopeName];
|
|
|
|
|
|
|
|
|
|
return Object.values(this.controllers).filter((controller) => {
|
|
|
|
|
if (controller.event.commandList.length === 0) return false;
|
|
|
|
|
if (eventGroup.commandList.length === 0) continue;
|
|
|
|
|
|
|
|
|
|
switch (senderInfo.type) {
|
|
|
|
|
case 'private':
|
|
|
|
|
if (!controller.event.allowPrivate) {
|
|
|
|
|
return false;
|
|
|
|
|
if (!eventGroup.allowPrivate) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!controller.event.isAllowSubscribe(senderInfo)) {
|
|
|
|
|
return false;
|
|
|
|
|
if (!eventGroup.isAllowSubscribe(senderInfo)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'group':
|
|
|
|
|
if (!controller.event.allowGroup) {
|
|
|
|
|
return false;
|
|
|
|
|
if (!eventGroup.allowGroup) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'channel':
|
|
|
|
|
if (!controller.event.allowChannel) {
|
|
|
|
|
return false;
|
|
|
|
|
if (!eventGroup.allowChannel) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (senderInfo.type !== 'private') { // 私聊消息不存在订阅,只判断群消息和频道消息
|
|
|
|
|
if (controller.event.autoSubscribe) {
|
|
|
|
|
if (!controller.event.isAllowSubscribe(senderInfo)) {
|
|
|
|
|
return false;
|
|
|
|
|
if (eventGroup.autoSubscribe) {
|
|
|
|
|
if (!eventGroup.isAllowSubscribe(senderInfo)) {
|
|
|
|
|
continue;
|
|
|
|
|
} else {
|
|
|
|
|
// 检测控制器是否已禁用
|
|
|
|
|
if (disabledControllers.includes(controller.id)) {
|
|
|
|
|
return false;
|
|
|
|
|
if (this.app.event.isPluginScopeInList(pluginInstance.id, scopeName, disabledScopes)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 检测控制器是否已启用
|
|
|
|
|
if (!subscribedControllers.includes(controller.id)) {
|
|
|
|
|
return false;
|
|
|
|
|
if (!this.app.event.isPluginScopeInList(pluginInstance.id, scopeName, subscribedScopes)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
eventGroups.push(eventGroup);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (eventGroups.length > 0) {
|
|
|
|
|
subscribed.push({
|
|
|
|
|
id: pluginInstance.id,
|
|
|
|
|
controller: pluginInstance.controller,
|
|
|
|
|
eventGroups: eventGroups
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>;
|
|
|
|
|
return subscribed;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class EventScope {
|
|
|
|
|
protected app: App;
|
|
|
|
|
protected eventManager: EventManager;
|
|
|
|
|
|
|
|
|
|
public pluginId: string;
|
|
|
|
|
public scopeName: string;
|
|
|
|
|
|
|
|
|
|
public commandList: CommandInfo[] = [];
|
|
|
|
|
public eventList: Record<string, EventListenerInfo[]> = {};
|
|
|
|
|
public eventSorted: Record<string, boolean> = {};
|
|
|
|
|
|
|
|
|
|
constructor(app: App) {
|
|
|
|
|
constructor(app: App, pluginId: string, scopeName: string) {
|
|
|
|
|
this.app = app;
|
|
|
|
|
this.eventManager = app.event;
|
|
|
|
|
|
|
|
|
|
this.pluginId = pluginId;
|
|
|
|
|
this.scopeName = scopeName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -515,8 +592,6 @@ export class EventScope {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class PluginEvent extends EventScope {
|
|
|
|
|
public controller?: PluginController;
|
|
|
|
|
|
|
|
|
|
public autoSubscribe = false;
|
|
|
|
|
public forceSubscribe = false;
|
|
|
|
|
public showInSubscribeList = true;
|
|
|
|
@ -552,10 +627,6 @@ export class PluginEvent extends EventScope {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public init(controller: PluginController) {
|
|
|
|
|
this.controller = controller;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Destroy eventGroup.
|
|
|
|
|
* Will remove all event listeners.
|
|
|
|
|