From f7e53aa87f2a7c01308107c2202cc82c63a2ebba Mon Sep 17 00:00:00 2001 From: Lex Lim Date: Sun, 19 Feb 2023 13:05:25 +0000 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8C=87=E4=BB=A4=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.ts | 15 +- src/Config.ts | 14 +- src/EventManager.ts | 159 ++++++++++++++--- src/PluginManager.ts | 59 +++--- src/RobotManager.ts | 2 +- src/SubscribeManager.ts | 100 +++++------ ...choController.ts => EchoController.ts.bak} | 12 +- src/controller/IsekaiWikiController.ts | 45 +++++ src/controller/SfsettingsController.ts | 42 +++++ src/controller/wiki/WikiMisc.ts | 168 ++++++++++++++++++ src/message/Message.ts | 3 + src/robot/QQRobot.ts | 19 +- 12 files changed, 519 insertions(+), 119 deletions(-) rename src/controller/{EchoController.ts => EchoController.ts.bak} (73%) create mode 100644 src/controller/IsekaiWikiController.ts create mode 100644 src/controller/SfsettingsController.ts create mode 100644 src/controller/wiki/WikiMisc.ts diff --git a/src/App.ts b/src/App.ts index 0698c1c..f65b143 100644 --- a/src/App.ts +++ b/src/App.ts @@ -19,6 +19,8 @@ export default class App { public config: Config; public srcPath: string = __dirname; + public debug: boolean = false; + public event!: EventManager; public robot!: RobotManager; public provider!: ProviderManager; @@ -30,6 +32,8 @@ export default class App { constructor(configFile: string) { this.config = Yaml.parse(fs.readFileSync(configFile, { encoding: 'utf-8' })); + this.debug = this.config.debug; + this.initialize(); } @@ -83,13 +87,6 @@ export default class App { async initChannelManager() { this.channel = new ChannelManager(this, this.config.channel_config_path); - this.channel.on('add', (channelId) => { - this.subscribe.addChannel(channelId); - }); - this.channel.on('remove', (channelId) => { - this.subscribe.removeChannel(channelId); - }); - await this.channel.initialize(); } @@ -111,8 +108,8 @@ export default class App { return this.provider.create(provider, channelId, config); } - getSubscriber(channelId: string, robotId: string): Target[] | null { - return this.subscribe.getSubscriber(channelId, robotId); + getChannelSubscriber(channelId: string, robotId: string): Target[] | null { + return this.subscribe.getSubscriber('channel:' + channelId, robotId); } /** diff --git a/src/Config.ts b/src/Config.ts index 0e8d2d6..666572a 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -6,9 +6,11 @@ export type Config = { plugin_path: string; subscribe_config: string; debug: boolean; - robot: { [key: string]: RobotConfig }; - service: { [key: string]: ServiceConfig }; + robot: Record; + service: Record; http_api: RestfulApiConfig; + command_override: CommandOverrideConfig; + focused_as_command: true; }; export type RobotConfig = { @@ -31,3 +33,11 @@ export type GeneratorConfig = { match: RegexFilterConfig; tpl: any; }; + +export type CommandOverrideConfig = { + [command: string]: { + name?: string; + help?: string; + alias?: string[]; + } +}; \ No newline at end of file diff --git a/src/EventManager.ts b/src/EventManager.ts index d7a6ef3..d903a69 100644 --- a/src/EventManager.ts +++ b/src/EventManager.ts @@ -1,19 +1,24 @@ import App from "./App"; import { CommonReceivedMessage, CommonSendMessage } from "./message/Message"; -import { GroupSender, UserSender } from "./message/Sender"; -import { ControllerSubscribeSource, MessageEventOptions, MessagePriority, PluginController } from "./PluginManager"; +import { CommandInfo, ControllerSubscribeSource, MessageEventOptions, MessagePriority, PluginController, PluginEvent } from "./PluginManager"; import { Robot } from "./RobotManager"; export type PluginControllerListenerInfo = { priority: number; callback: CallableFunction; - controller: PluginController; + controllerEvent: PluginEvent; +} + +export type PluginControllerCommandInfo = { + commandInfo: CommandInfo; + controllerEvent: PluginEvent; } export class EventManager { private app: App; private eventSortDebounce: Record = {}; private eventList: Record = {}; + private commandList: Record = {}; constructor(app: App) { this.app = app; @@ -23,7 +28,7 @@ export class EventManager { } - on(event: string, controller: PluginController, callback: CallableFunction, options?: MessageEventOptions) { + public on(event: string, controllerEvent: PluginEvent, callback: CallableFunction, options?: MessageEventOptions) { if (!(event in this.eventList)) { this.eventList[event] = []; } @@ -43,7 +48,7 @@ export class EventManager { const eventInfo = { callback: callback, priority: options.priority!, - controller: controller + controllerEvent }; this.eventList[event].push(eventInfo); @@ -51,42 +56,112 @@ export class EventManager { this.sortEvent(event); } - off(event: string, controller: PluginController, callback: CallableFunction): void - off(controller: PluginController): void - off(...args: any): void { + public off(event: string, controllerEvent: PluginEvent, callback: CallableFunction): void + public off(controllerEvent: 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.controller !== controller); + this.eventList[event] = this.eventList[event].filter((eventInfo) => eventInfo.callback !== callback || eventInfo.controllerEvent !== 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.controller !== controller); + this.eventList[event] = this.eventList[event].filter((eventInfo) => eventInfo.controllerEvent !== controller); } } } - public async emit(eventName: string, senderInfo: ControllerSubscribeSource, ...args: any[]) { + public addCommand(commandInfo: CommandInfo, controllerEvent: PluginEvent) { + let data = { + commandInfo, + controllerEvent: controllerEvent + }; + this.commandList[commandInfo.command] = data; + if (Array.isArray(commandInfo.alias)) { + commandInfo.alias.forEach((alias) => { + this.commandList[alias] = data; + }); + } + } + + public removeCommand(commandInfo: CommandInfo): void + public removeCommand(controllerEvent: PluginEvent): void + public removeCommand(...args: any): void { + if ('command' in args[0]) { + let commandInfo: CommandInfo = args[0]; + delete this.commandList[commandInfo.command]; + if (Array.isArray(commandInfo.alias)) { + commandInfo.alias.forEach((alias) => { + delete this.commandList[alias]; + }); + } + } else if (typeof args[0] !== 'undefined') { + let controllerEvent = args[0]; + for (let command in this.commandList) { + if (this.commandList[command].controllerEvent.controller?.id === controllerEvent.controller?.id) { + delete this.commandList[command]; + } + } + } + } + + public async emit(eventName: string, senderInfo?: ControllerSubscribeSource | null, ...args: any[]) { + if (this.app.debug) { + if (args[0] instanceof CommonReceivedMessage) { + console.log(`[DEBUG] 触发事件 ${eventName} ${args[0].contentText}`); + } else { + console.log(`[DEBUG] 触发事件 ${eventName}`); + } + } + const eventList = this.eventList[eventName]; if (!eventList) return false; + const isFilter = eventName.startsWith('filter/'); + let isResolved = false; const resolved = () => { isResolved = true; }; + let subscribeList: string[] = []; + + 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; + } + subscribeList = this.app.subscribe.getSubscribedList(senderInfo.robot.robotId!, targetType, targetId, 'controller'); + } + for (let eventInfo of eventList) { - if (eventInfo.controller.autoSubscribe) { - if (!eventInfo.controller.isAllowSubscribe(senderInfo)) { - continue; - } else { - // 需要添加订阅检测 + if (!isFilter && senderInfo) { + if (eventInfo.controllerEvent.autoSubscribe) { + if (!eventInfo.controllerEvent.isAllowSubscribe(senderInfo)) { + continue; + } else { + // 需要添加订阅检测 + } + } else if (senderInfo.type !== 'private') { + if (!eventInfo.controllerEvent.controller || !subscribeList.includes(eventInfo.controllerEvent.controller.id)) { + continue; + } } - } else { - // 需要添加订阅检测 - continue; } try { @@ -107,6 +182,11 @@ export class EventManager { let isResolved = false; if (message.origin === 'private' || (message.origin === '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; } @@ -120,7 +200,41 @@ export class EventManager { return false; } - public async emitCommand(command: string, args: string, message: CommonReceivedMessage) { + public async emitCommand(contentText: string, message: CommonReceivedMessage) { + let command = ''; + let args = ''; + + // 尝试识别空格分隔的指令 + if (contentText.includes(' ')) { + command = contentText.split(' ')[0]; + args = 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; + } + + args = contentText.substring(command.length); + } + + if (this.app.debug) { + console.log('[DEBUG] 指令识别结果', command, args); + } + return await this.emit(`command/${command}`, this.getSenderInfo(message), args, message); } @@ -130,7 +244,12 @@ export class EventManager { public async emitRawMessage(message: 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); } diff --git a/src/PluginManager.ts b/src/PluginManager.ts index fbbd0e1..9559cce 100644 --- a/src/PluginManager.ts +++ b/src/PluginManager.ts @@ -89,7 +89,7 @@ export class PluginManager extends EventEmitter { } async loadController(file: string) { - if (!file.match(/\.m?js$/)) return; + if (!file.match(/Controller\.m?js$/)) return; let moduleName = path.resolve(file).replace(/\\/g, '/').replace(/\.m?js$/, ''); @@ -97,7 +97,7 @@ export class PluginManager extends EventEmitter { const controller = await import(moduleName); if (controller) { const controllerClass = controller.default ?? controller; - const controllerInstance: PluginController = new controllerClass(this.app, this.app.event); + const controllerInstance: PluginController = new controllerClass(this.app); if (controllerInstance.id && controllerInstance.id !== '') { const controllerId = controllerInstance.id; @@ -118,6 +118,8 @@ export class PluginManager extends EventEmitter { this.emit('controllerLoaded', controllerInstance); } + const pluginMisc = new PluginEvent(this.app.event); + controllerInstance.event = pluginMisc; await controllerInstance.initialize(); } else { throw new Error('PluginController ID is not defined.'); @@ -134,7 +136,8 @@ export class PluginManager extends EventEmitter { async removeController(file: string, isReload = false) { const controller = this.fileControllers[file]; if (controller) { - await controller.destroy(); + await controller.event.destroy(); + await controller.destroy?.(); delete this.controllers[file]; delete this.fileControllers[file]; @@ -147,14 +150,22 @@ export class PluginManager extends EventEmitter { } } -export class PluginController { - public id: string = ''; - public name: string = '未命名功能'; - public description: string = ''; +export interface PluginController { + id: string; + name: string; + description?: string; - private app: App; + event: PluginEvent; + + initialize: () => Promise; + destroy?: () => Promise; +} + +export class PluginEvent { private eventManager: EventManager; + public controller?: PluginController; + public autoSubscribe = false; public forceSubscribe = false; public showInSubscribeList = true; @@ -168,8 +179,7 @@ export class PluginController { private commandList: CommandInfo[] = []; private eventList: Record = {}; - constructor(app: App, eventManager: EventManager) { - this.app = app; + constructor(eventManager: EventManager) { this.eventManager = eventManager; } @@ -204,21 +214,21 @@ export class PluginController { * @param callback Callback function * @param options Options */ - protected on(event: 'message/private', callback: MessageCallback, options?: MessageEventOptions): void + public 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 + public 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 + public 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 @@ -226,7 +236,7 @@ export class PluginController { * @param callback Callback function * @param options Options */ - protected on(event: 'message/focused', callback: MessageCallback, options?: MessageEventOptions): void + public on(event: 'message/focused', callback: MessageCallback, options?: MessageEventOptions): void /** * Add message handler. * Will handle all messages (group, private, channel) @@ -234,7 +244,7 @@ export class PluginController { * @param callback Callback function * @param options Options */ - protected on(event: 'message', callback: MessageCallback, options?: MessageEventOptions): void + public on(event: 'message', callback: MessageCallback, options?: MessageEventOptions): void /** * Add raw message handler. * Will be triggered even when the message is a command. @@ -242,28 +252,28 @@ export class PluginController { * @param callback Callback function * @param options Options */ - protected on(event: 'raw/message', callback: MessageCallback, options?: MessageEventOptions): void + public 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 + public 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 + public 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 { + public on(event: string, callback: CallableFunction, options?: MessageEventOptions): void { if (!(event in this.eventList)) { this.eventList[event] = []; } @@ -289,7 +299,7 @@ export class PluginController { this.eventManager.on(event, this, callback, options); } - protected off(event: string, callback: CallableFunction): void { + public off(event: string, callback: CallableFunction): void { if (Array.isArray(this.eventList[event])) { this.eventList[event] = this.eventList[event].filter((eventInfo) => { return eventInfo.callback !== callback; @@ -306,9 +316,9 @@ export class PluginController { * @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 { + public registerCommand(command: string, name: string, callback: CommandCallback, options?: MessageEventOptions): void + public registerCommand(commandInfo: CommandInfo, callback: CommandCallback, options?: MessageEventOptions): void + public registerCommand(...args: any[]): void { // 处理传入参数 let commandInfo: Partial = {}; let callback: MessageCallback; @@ -334,6 +344,8 @@ export class PluginController { this.on(`command/${cmd}`, callback, options); }); } + + this.eventManager.addCommand(commandInfo as any, this); } /** @@ -349,6 +361,7 @@ export class PluginController { */ public async destroy() { this.eventManager.off(this); + this.eventManager.removeCommand(this); this.eventList = {}; } diff --git a/src/RobotManager.ts b/src/RobotManager.ts index 13a3d78..2236ff6 100644 --- a/src/RobotManager.ts +++ b/src/RobotManager.ts @@ -98,7 +98,7 @@ export class RobotManager { continue; } - let targets = this.app.getSubscriber(channelId, robotId); + let targets = this.app.getChannelSubscriber(channelId, robotId); if (!targets) { continue; } diff --git a/src/SubscribeManager.ts b/src/SubscribeManager.ts index 63e63f3..3b4c704 100644 --- a/src/SubscribeManager.ts +++ b/src/SubscribeManager.ts @@ -9,27 +9,31 @@ export interface Target { identity: string; } +export type SubscribeConfig = { + [robotId: string]: { + [targetType: string]: { + [targetIdentity: string]: { + [sourceType: string]: string[] + } + } + } +} + /** * 订阅管理 - * @todo 取消通配符支持,仅支持订阅单频道和频道组下的单层频道 */ export class SubscribeManager { private app: App; private subscribeFile: string; private watcher!: chokidar.FSWatcher; + private subscribeList: { - [channelId: string]: { + [sourceId: string]: { [robotId: string]: Target[] } }; - private subscribeConfig: { - [robotId: string]: { - [targetType: string]: { - [targetIdentity: string]: string[] - } - } - }; + private subscribeConfig: SubscribeConfig; constructor(app: App, subscribeFile: string) { this.app = app; @@ -76,8 +80,15 @@ export class SubscribeManager { let targetTypeConf = targetConf[targetType]; for (let targetId in targetTypeConf) { let subscribeList = targetTypeConf[targetId]; - for (let channelId of subscribeList) { - this.addSubscribe(robotId, targetType, targetId, channelId); + if (subscribeList.channel) { + for (let sourceId in subscribeList.channel) { + this.addSubscribe(robotId, targetType, targetId, 'channel:' + sourceId); + } + } + if (subscribeList.controller) { + for (let controllerId in subscribeList.controller) { + this.addSubscribe(robotId, targetType, targetId, 'controller:' + controllerId); + } } } } @@ -87,15 +98,15 @@ export class SubscribeManager { /** * 初始化订阅树 * @param robotId - * @param channelId + * @param sourceId */ - public prepareTree(robotId: string, channelId: string) { - if (!(channelId in this.subscribeList)) { - this.subscribeList[channelId] = {}; + public prepareTree(robotId: string, sourceId: string) { + if (!(sourceId in this.subscribeList)) { + this.subscribeList[sourceId] = {}; } - if (!(robotId in this.subscribeList[channelId])) { - this.subscribeList[channelId][robotId] = []; + if (!(robotId in this.subscribeList[sourceId])) { + this.subscribeList[sourceId][robotId] = []; } } @@ -104,11 +115,11 @@ export class SubscribeManager { * @param robotId 机器人ID * @param targetType 目标类型 * @param targetId 目标ID - * @param channelId 频道ID + * @param sourceId 订阅源ID */ - public addSubscribe(robotId: string, targetType: string, targetId: string, channelId: string) { - this.prepareTree(robotId, channelId); - this.subscribeList[channelId][robotId].push({ + public addSubscribe(robotId: string, targetType: string, targetId: string, sourceId: string) { + this.prepareTree(robotId, sourceId); + this.subscribeList[sourceId][robotId].push({ type: targetType, identity: targetId }); @@ -119,47 +130,32 @@ export class SubscribeManager { * @param robotId 机器人ID * @param targetType 目标类型 * @param targetId 目标ID - * @param channelId 频道ID + * @param sourceId 订阅源ID */ - public removeSubscribe(robotId: string, targetType: string, targetId: string, channelId: string) { - if (this.subscribeList?.[channelId]?.[robotId]) { - this.subscribeList[channelId][robotId] = this.subscribeList[channelId][robotId].filter((target) => { + public removeSubscribe(robotId: string, targetType: string, targetId: string, sourceId: string) { + if (this.subscribeList?.[sourceId]?.[robotId]) { + this.subscribeList[sourceId][robotId] = this.subscribeList[sourceId][robotId].filter((target) => { return (target.type !== targetType || targetId != targetId); }); } } /** - * 添加频道并更新订阅列表 - * @param channelId 频道ID - */ - public addChannel(channelId: string) { - - } - - /** - * 移除频道并更新订阅列表 - * @param channelId 频道ID - */ - public removeChannel(channelId: string) { - - } - - /** - * 获取频道订阅者 - * @param channelId 频道ID + * 获取订阅者 + * @param sourceId 订阅源ID * @param robotId 机器人ID * @returns */ - public getSubscriber(channelId: string, robotId: string): Target[] | null { + public getSubscriber(sourceId: string, robotId: string): Target[] | null { let subscribers: Target[] = []; - // 先获取频道本身的订阅 - if (this.subscribeList?.[channelId]?.[robotId]) { - subscribers.push(...this.subscribeList[channelId][robotId]); + // 获取订阅 + if (this.subscribeList?.[sourceId]?.[robotId]) { + subscribers.push(...this.subscribeList[sourceId][robotId]); } - // 获取父级(频道组)的订阅 - if (channelId.includes('/')) { - let channelGroupPath = channelId.substring(0, channelId.lastIndexOf('/')); + + if (sourceId.startsWith('channel:') && sourceId.includes('/')) { + // 获取父级(频道组)的订阅 + let channelGroupPath = sourceId.substring(0, sourceId.lastIndexOf('/')); if (this.subscribeList?.[channelGroupPath]?.[robotId]) { subscribers.push(...this.subscribeList[channelGroupPath][robotId]); } @@ -171,4 +167,8 @@ export class SubscribeManager { return null; } } + + public getSubscribedList(robotId: string, targetType: string, targetId: string, sourceType: string): string[] { + return this.subscribeConfig?.[robotId]?.[targetType]?.[targetId]?.[sourceType] ?? []; + } } diff --git a/src/controller/EchoController.ts b/src/controller/EchoController.ts.bak similarity index 73% rename from src/controller/EchoController.ts rename to src/controller/EchoController.ts.bak index 0efc81e..20fd653 100644 --- a/src/controller/EchoController.ts +++ b/src/controller/EchoController.ts.bak @@ -1,15 +1,17 @@ import { CommonReceivedMessage } from "../message/Message"; -import { PluginController } from "../PluginManager"; +import { PluginController, PluginEvent } from "../PluginManager"; + +export default class EchoController implements PluginController { + public event!: PluginEvent; -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); + this.event.autoSubscribe = true; + + this.event.on("message/focused", this.handleEcho); } private async handleEcho(message: CommonReceivedMessage, resolved: CallableFunction) { diff --git a/src/controller/IsekaiWikiController.ts b/src/controller/IsekaiWikiController.ts new file mode 100644 index 0000000..c1e8849 --- /dev/null +++ b/src/controller/IsekaiWikiController.ts @@ -0,0 +1,45 @@ +import App from "../App"; +import { PluginController, PluginEvent } from "../PluginManager"; +import { WikiMisc } from "./wiki/WikiMisc"; + +const API_ENDPOINT = 'https://www.isekai.cn/api.php'; + +export default class IsekaiWikiController implements PluginController { + public event!: PluginEvent; + public app: App; + + public apiEndpoint = 'https://www.isekai.cn/api.php'; + + public id = 'isekaiwiki'; + public name = '异世界百科'; + public description = '异世界百科的相关功能'; + + constructor(app: App) { + this.app = app; + } + + public async initialize(): Promise { + this.event.controller = this; + + const wikiMisc = new WikiMisc(this.app, 'https://www.isekai.cn/api.php'); + + this.event.registerCommand({ + command: '百科', + name: '搜索异世界百科', + }, (args, message, resolved) => { + resolved(); + + wikiMisc.handleSearch(args, message); + }); + + this.event.registerCommand({ + command: '随机', + name: '获取随机的百科页面', + alias: ['随机词条', '随机页面'], + }, (args, message, resolved) => { + resolved(); + + wikiMisc.handleRandomPage(args, message); + }); + } +} \ No newline at end of file diff --git a/src/controller/SfsettingsController.ts b/src/controller/SfsettingsController.ts new file mode 100644 index 0000000..ed0b8fb --- /dev/null +++ b/src/controller/SfsettingsController.ts @@ -0,0 +1,42 @@ +import App from "../App"; +import { PluginController, PluginEvent } from "../PluginManager"; +import { WikiMisc } from "./wiki/WikiMisc"; + +export default class SfsettingsController implements PluginController { + public event!: PluginEvent; + public app: App; + + public id = 'sfsettings'; + public name = '科幻设定百科'; + public description = '科幻设定百科的相关功能'; + + constructor(app: App) { + this.app = app; + } + + public async initialize(): Promise { + this.event.controller = this; + + const wikiMisc = new WikiMisc(this.app, 'https://www.sfsettings.com/w/api.php'); + + this.event.registerCommand({ + command: '百科', + name: '在百科上搜索', + alias: ['搜索', '查找', '词条'], + }, (args, message, resolved) => { + resolved(); + + wikiMisc.handleSearch(args, message); + }); + + this.event.registerCommand({ + command: '随机', + name: '获取随机的百科页面', + alias: ['随机词条', '随机页面'], + }, (args, message, resolved) => { + resolved(); + + wikiMisc.handleRandomPage(args, message); + }); + } +} \ No newline at end of file diff --git a/src/controller/wiki/WikiMisc.ts b/src/controller/wiki/WikiMisc.ts new file mode 100644 index 0000000..d1cfd53 --- /dev/null +++ b/src/controller/wiki/WikiMisc.ts @@ -0,0 +1,168 @@ +import request from "request-promise"; +import App from "../../App"; +import { CommonReceivedMessage } from "../../message/Message"; +import { PluginEvent } from "../../PluginManager"; + +export class WikiMisc { + public event!: PluginEvent; + public app: App; + + private apiEndpoint: string; + + public id = 'sfsettings'; + public name = '科幻设定百科'; + public description = '科幻设定百科的相关功能'; + + constructor(app: App, apiEndpoint: string) { + this.app = app; + this.apiEndpoint = apiEndpoint; + } + + public async handleSearch(args: string, message: CommonReceivedMessage) { + try { + let res = await request({ + uri: this.apiEndpoint, + method: 'GET', + qs: { + action: 'opensearch', + search: args, + limit: 10, + namespace: 0, + format: 'json', + formatversion: 2, + }, + json: true, + }); + + if (res.error) { + message.sendReply('获取词条列表失败: ' + res.error.info, true); + } + + let titles = res[1]; + if (titles.length === 0) { + message.sendReply('未找到相关词条', true); + return; + } + + // Get page info + res = await request({ + uri: this.apiEndpoint, + method: 'GET', + qs: { + action: 'query', + prop: 'info|extracts', + inprop: 'url', + exintro: true, + explaintext: true, + exsentences: 3, + exlimit: 1, + redirects: true, + format: 'json', + formatversion: 2, + titles: titles[0], + }, + json: true, + }); + + if (res.error) { + message.sendReply('获取词条详情失败: ' + res.error.info, true); + return; + } + + let pages = res.query.pages; + let page = pages[0]; + if (page.missing) { + message.sendReply('未找到相关词条', true); + return; + } + + let reply = '找到的词条:' + titles.join('、') + '\n'; + reply += '《' + page.title + '》\n'; + reply += page.extract; + + message.sendReply(reply, true); + } catch (err: any) { + message.sendReply('获取词条详情失败: ' + err.message, true); + console.error(err); + } + } + + public async handleRandomPage(args: string, message: CommonReceivedMessage) { + try { + let res = await request({ + uri: this.apiEndpoint, + method: 'GET', + qs: { + action: 'query', + prop: 'info|extracts', + inprop: 'url', + exintro: true, + explaintext: true, + exsentences: 3, + exlimit: 1, + list: 'random', + rnnamespace: 0, + rnlimit: 1, + format: 'json', + formatversion: 2, + }, + json: true, + }); + + if (res.error) { + message.sendReply('获取随机页面失败: ' + res.error.info, true); + return; + } + + if (this.app.debug) { + console.log(res); + } + + let pageTitle = res.query.random?.[0]?.title; + if (!pageTitle) { + message.sendReply('未找到随机页面', true); + return; + } + // Get page info + res = await request({ + uri: this.apiEndpoint, + method: 'GET', + qs: { + action: 'query', + prop: 'info|extracts', + inprop: 'url', + exintro: true, + explaintext: true, + exsentences: 3, + exlimit: 1, + redirects: true, + format: 'json', + formatversion: 2, + titles: pageTitle, + }, + json: true, + }); + + if (res.error) { + message.sendReply('获取随机页面失败: ' + res.error.info, true); + return; + } + + let pages = res.query.pages; + let page = pages[0]; + if (!page || page.missing) { + message.sendReply('获取随机页面失败:页面丢失', true); + return; + } + + let reply = '《' + page.title + '》\n'; + reply += page.extract + '\n'; + reply += page.canonicalurl; + + message.sendReply(reply, true); + } catch (err: any) { + message.sendReply('获取随机页面失败: ' + err.message, true); + console.error(err); + } + } +} \ No newline at end of file diff --git a/src/message/Message.ts b/src/message/Message.ts index bdd07a0..3fbb1cb 100644 --- a/src/message/Message.ts +++ b/src/message/Message.ts @@ -63,6 +63,9 @@ export class CommonMessage { /** 提到的人 */ mentions?: { uid: string, text?: string }[]; + /** 附加信息 */ + extra: any = {}; + private _contentText?: string; public get contentText() { diff --git a/src/robot/QQRobot.ts b/src/robot/QQRobot.ts index 5c69644..39e6c2d 100644 --- a/src/robot/QQRobot.ts +++ b/src/robot/QQRobot.ts @@ -140,10 +140,10 @@ export default class QQRobot implements Robot { if (isResolved) return; // 处理指令 - let commandInfo = this.getCommandInfo(message); - if (commandInfo) { - isResolved = await this.app.event.emitCommand(commandInfo.name, commandInfo.args, message); - if (isResolved) return; + let commandText = this.getCommandContentText(message); + if (commandText) { + await this.app.event.emitCommand(commandText, message); + return; } // 处理消息 @@ -153,12 +153,10 @@ export default class QQRobot implements Robot { } } - getCommandInfo(message: CommonReceivedMessage) { + getCommandContentText(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 message.contentText.substring(prefix.length); } } return null; @@ -211,9 +209,12 @@ export default class QQRobot implements Robot { let msgData = await convertMessageToQQChunk(message); if (message.origin === 'private') { + if (this.app.debug) console.log('[DEBUG] 发送私聊消息', message.targetId, msgData); + await this.sendToUser(message.targetId, msgData); } else if (message.origin === 'group') { - console.log('发送群消息', message.targetId, msgData); + if (this.app.debug) console.log('[DEBUG] 发送群消息', message.targetId, msgData); + await this.sendToGroup(message.targetId, msgData); }