完成基本的事件和控制器加载功能

main
落雨楓 2 years ago
parent 484bc90692
commit c695a18587

@ -7,6 +7,7 @@ import { ChannelManager } from './ChannelManager';
import { ChannelConfig, Config } from './Config'; import { ChannelConfig, Config } from './Config';
import { EventManager } from './EventManager'; import { EventManager } from './EventManager';
import { CommonSendMessage } from './message/Message'; import { CommonSendMessage } from './message/Message';
import { PluginManager } from './PluginManager';
import { ProviderManager } from './ProviderManager'; import { ProviderManager } from './ProviderManager';
import { RestfulApiManager } from './RestfulApiManager'; import { RestfulApiManager } from './RestfulApiManager';
import { RobotManager } from './RobotManager'; import { RobotManager } from './RobotManager';
@ -24,6 +25,7 @@ export default class App {
public service!: ServiceManager; public service!: ServiceManager;
public subscribe!: SubscribeManager; public subscribe!: SubscribeManager;
public channel!: ChannelManager; public channel!: ChannelManager;
public plugin!: PluginManager;
public restfulApi!: RestfulApiManager; public restfulApi!: RestfulApiManager;
constructor(configFile: string) { constructor(configFile: string) {
@ -40,6 +42,7 @@ export default class App {
await this.initServiceManager(); await this.initServiceManager();
await this.initSubscribeManager(); await this.initSubscribeManager();
await this.initChannelManager(); await this.initChannelManager();
await this.initPluginManager();
console.log('初始化完成,正在接收消息'); console.log('初始化完成,正在接收消息');
} }
@ -77,10 +80,6 @@ export default class App {
await this.subscribe.initialize(); await this.subscribe.initialize();
} }
async initCommandManager() {
}
async initChannelManager() { async initChannelManager() {
this.channel = new ChannelManager(this, this.config.channel_config_path); this.channel = new ChannelManager(this, this.config.channel_config_path);
@ -94,9 +93,14 @@ export default class App {
await this.channel.initialize(); await this.channel.initialize();
} }
async initPluginManager() {
this.plugin = new PluginManager(this, this.config.plugin_path);
await this.plugin.initialize();
}
/** /**
* *
* @param serviceName * @param serviceName
* @returns * @returns
*/ */
getService<T extends Service>(serviceName: string): T { getService<T extends Service>(serviceName: string): T {
@ -122,14 +126,6 @@ export default class App {
this.robot.sendPushMessage(channelId, messages); this.robot.sendPushMessage(channelId, messages);
} }
/**
*
* @param message
*/
async sendMessage(message: CommonSendMessage) {
}
require(file: string): any { require(file: string): any {
return require(path.join(this.srcPath, file)); return require(path.join(this.srcPath, file));
} }

@ -12,8 +12,6 @@ import { ChannelConfig } from './Config';
export class ChannelManager extends EventEmitter { export class ChannelManager extends EventEmitter {
private app: App; private app: App;
private channelPath: string; private channelPath: string;
private loadChannelCallback: (file: string) => any;
private removeChannelCallback: (file: string) => any;
private setLoading?: debounce<Function>; private setLoading?: debounce<Function>;
private watcher!: chokidar.FSWatcher; private watcher!: chokidar.FSWatcher;
@ -27,9 +25,6 @@ export class ChannelManager extends EventEmitter {
this.channelPath = channelPath; this.channelPath = channelPath;
this.channels = {}; this.channels = {};
this.channelName = {}; 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 persistent: true
}); });
this.watcher.on('add', this.loadChannelCallback); this.watcher.on('add', this.loadChannel.bind(this));
this.watcher.on('change', this.loadChannelCallback); this.watcher.on('change', this.loadChannel.bind(this));
this.watcher.on('unlink', this.removeChannelCallback); this.watcher.on('unlink', this.loadChannel.bind(this));
} }
/** /**

@ -3,6 +3,7 @@ import { RegexFilterConfig } from "./generator/RegexFilter";
export type Config = { export type Config = {
channel_config_path: string; channel_config_path: string;
plugin_path: string;
subscribe_config: string; subscribe_config: string;
debug: boolean; debug: boolean;
robot: { [key: string]: RobotConfig }; robot: { [key: string]: RobotConfig };

@ -1,253 +1,174 @@
import EventEmitter from "events";
import { debounce } from "throttle-debounce";
import App from "./App"; import App from "./App";
import { CommonReceivedMessage } from "./message/Message"; import { CommonReceivedMessage, CommonSendMessage } from "./message/Message";
import { GroupSender, UserSender } from "./message/Sender"; import { GroupSender, UserSender } from "./message/Sender";
import { ControllerSubscribeSource, MessageEventOptions, MessagePriority, PluginController } from "./PluginManager";
import { Robot } from "./RobotManager"; import { Robot } from "./RobotManager";
export type PluginControllerListenerInfo = {
priority: number;
callback: CallableFunction;
controller: PluginController;
}
export class EventManager { export class EventManager {
private app: App; private app: App;
private eventEmitter: EventEmitter; private eventSortDebounce: Record<string, NodeJS.Timeout> = {};
private eventGroup: Record<string, EventGroup> = {}; private eventList: Record<string, PluginControllerListenerInfo[]> = {};
private eventHandlerList: Record<string, EventGroup[]> = {};
constructor(app: App) { constructor(app: App) {
this.app = app; this.app = app;
this.eventEmitter = new EventEmitter;
} }
public async initialize() { public async initialize() {
} }
public getEventGroup() { on(event: string, controller: PluginController, callback: CallableFunction, options?: MessageEventOptions) {
if (!(event in this.eventList)) {
} this.eventList[event] = [];
} }
export const MessagePriority = { let defaultOptions: MessageEventOptions = {
LOWEST: 0, priority: MessagePriority.DEFAULT
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[],
}; };
if (!options) {
export type CommandInfo = { options = defaultOptions;
command: string, } else {
name: string, options = {
alias?: string[], ...defaultOptions,
help?: string, ...options
}; };
export type EventListenerInfo = {
priority: number;
callback: CallableFunction;
} }
export type MessageCallback = (message: CommonReceivedMessage, resolved: VoidFunction) => any; const eventInfo = {
export type CommandCallback = (argv: string[], message: CommonReceivedMessage, resolved: VoidFunction) => any; callback: callback,
export type RawEventCallback = (robot: Robot, event: any, resolved: VoidFunction) => any; priority: options.priority!,
controller: controller
export type AllowedList = string[] | '*'; };
export class EventGroup {
readonly id: string;
public allowPrivate = true;
public allowGroup = true;
public allowedGroupList: AllowedList = '*';
private commandList: CommandInfo[] = []; this.eventList[event].push(eventInfo);
private eventList: Record<string, EventListenerInfo[]> = {};
constructor(id: string) { this.sortEvent(event);
this.id = id;
} }
public shouldAllowSource: (sender: any) => boolean = (sender) => { off(event: string, controller: PluginController, callback: CallableFunction): void
if (sender instanceof UserSender) { off(controller: PluginController): void
if (this.allowPrivate) { off(...args: any): void {
return true; 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) { } else if (typeof args[0] !== 'undefined') {
if (this.allowedGroupList === '*') { let controller = args[0];
return true; for (let event in this.eventList) {
} else if (this.allowedGroupList.includes(sender.groupId)) { this.eventList[event] = this.eventList[event].filter((eventInfo) => eventInfo.controller !== controller);
return true;
} }
} }
return false;
} }
public emit(eventName: string, ...args: any[]) { public async emit(eventName: string, senderInfo: ControllerSubscribeSource, ...args: any[]) {
const eventList = this.eventList[eventName]; const eventList = this.eventList[eventName];
if (!eventList) return false;
let isResolved = false; let isResolved = false;
let isBreakAll = false;
const resolved = (breakAll: boolean = false) => { const resolved = () => {
isResolved = true; isResolved = true;
if (breakAll) {
isBreakAll = true;
}
}; };
for (let eventInfo of eventList) { for (let eventInfo of eventList) {
eventInfo.callback(...args, resolved); if (eventInfo.controller.autoSubscribe) {
if (!eventInfo.controller.isAllowSubscribe(senderInfo)) {
continue;
} else {
// 需要添加订阅检测
}
} else {
// 需要添加订阅检测
continue;
}
try {
await eventInfo.callback(...args, resolved);
if (isResolved) { if (isResolved) {
break; break;
} }
} catch(err: any) {
console.error(`事件 ${eventName} 处理失败`);
console.error(err);
} }
return !isBreakAll;
} }
on(event: string, callback: CallableFunction, options?: MessageEventOptions) { return isResolved;
if (!(event in this.eventList)) {
this.eventList[event] = [];
} }
let defaultOptions: MessageEventOptions = { public async emitMessage(message: CommonReceivedMessage) {
priority: MessagePriority.DEFAULT let isResolved = false;
};
if (!options) { if (message.origin === 'private' || (message.origin === 'group' && message.mentionedReceiver)) {
options = defaultOptions; isResolved = await this.emit(`message/focused`, this.getSenderInfo(message), message);
} else { if (isResolved) return true;
options = {
...defaultOptions,
...options
};
} }
const eventInfo = { isResolved = await this.emit(`message/${message.origin}`, this.getSenderInfo(message), message);
callback: callback, if (isResolved) return true;
priority: options.priority!
};
const singleEventList = this.eventList[event];
const priority = options.priority!;
// Add event to specified position isResolved = await this.emit('message', this.getSenderInfo(message), message);
if (singleEventList.length === 0) { if (isResolved) return true;
singleEventList.push(eventInfo);
} else { return false;
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)
];
} }
public async emitCommand(command: string, args: string, message: CommonReceivedMessage) {
return await this.emit(`command/${command}`, this.getSenderInfo(message), args, 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: 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);
} }
public async emitFilterSendMessage(message: CommonSendMessage) {
} }
addCommand(command: string, name: string, callback: MessageCallback, options?: MessageEventOptions): void public getSenderInfo(message: CommonReceivedMessage): ControllerSubscribeSource {
addCommand(commandInfo: CommandInfo, callback: MessageCallback, options?: MessageEventOptions): void if (message.origin === 'private') {
addCommand(...args: any[]): void { return {
// 处理传入参数 type: 'private',
let commandInfo: Partial<CommandInfo> = {}; robot: message.receiver,
let callback: MessageCallback; userId: message.sender.uid
let options: MessageEventOptions;
if (typeof args[0] === 'string' && typeof args[1] === 'string') {
commandInfo = {
command: args[0],
name: args[1]
}; };
callback = args[2]; } else if (message.origin === 'group') {
options = args[3] ?? {}; return {
} else { type: 'group',
commandInfo = args[0]; robot: message.receiver,
callback = args[1]; groupId: message.sender.groupId,
options = args[2] ?? {}; userId: message.sender.uid
} };
}
// 注册消息
this.commandList.push(commandInfo as any); return {
this.on(`command/${commandInfo.command}`, callback, options); type: 'unknown',
if (Array.isArray(commandInfo.alias)) { // Add event for alias robot: message.receiver
commandInfo.alias.forEach((cmd) => { }
this.on(`command/${cmd}`, callback, options); }
});
} private sortEvent(eventName: string) {
} if (this.eventSortDebounce[eventName]) {
return;
/** }
* Add message handle.
* will be trigger on private message or group message with mentions to robot this.eventSortDebounce[eventName] = setTimeout(() => {
* @param callback this.eventList[eventName] = this.eventList[eventName].sort((a, b) => b.priority - a.priority);
* @param options
*/ delete this.eventSortDebounce[eventName];
onMentionedMessage(callback: MessageCallback, options?: MessageEventOptions) { }, 200);
this.on('mentionedMessage', callback, options);
this.on('privateMessage', callback, options);
}
/**
* Add private message handle.
* @param callback
* @param options
*/
onPrivateMessage(callback: MessageCallback, options?: MessageEventOptions) {
this.on('privateMessage', callback, options);
}
/**
* Add group message handle.
* @param callback
* @param options
*/
onGroupMessage(callback: MessageCallback, options?: MessageEventOptions) {
this.on('groupMessage', callback, options);
}
/**
* Add message handle.
* Will handle all messages in group
* @param callback
* @param options
*/
onMessage(callback: MessageCallback, options?: MessageEventOptions) {
this.on('message', callback, options);
}
/**
* 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);
}
onRawEvent(callback: RawEventCallback, options?: MessageEventOptions) {
this.on('rawEvent', callback, options);
} }
} }

@ -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<string, PluginController>;
public fileControllers: Record<string, PluginController>;
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<string, EventListenerInfo[]> = {};
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<CommandInfo> = {};
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 = {};
}
}

@ -5,6 +5,7 @@ import App from "./App";
import { MultipleMessage } from "./base/provider/BaseProvider"; import { MultipleMessage } from "./base/provider/BaseProvider";
import { RobotConfig } from "./Config"; import { RobotConfig } from "./Config";
import { CommonSendMessage } from "./message/Message"; import { CommonSendMessage } from "./message/Message";
import { CommandInfo } from "./PluginManager";
import { RestfulApiManager, RestfulContext, RestfulRouter } from "./RestfulApiManager"; import { RestfulApiManager, RestfulContext, RestfulRouter } from "./RestfulApiManager";
import { Target } from "./SubscribeManager"; import { Target } from "./SubscribeManager";
@ -16,6 +17,7 @@ export interface Robot {
uid?: string; uid?: string;
initialize?: () => Promise<any>; initialize?: () => Promise<any>;
initRestfulApi?: (router: RestfulRouter, api: RestfulApiManager) => Promise<any>; initRestfulApi?: (router: RestfulRouter, api: RestfulApiManager) => Promise<any>;
setCommands?(commands: CommandInfo[]): Promise<any>;
sendMessage(message: CommonSendMessage): Promise<CommonSendMessage>; sendMessage(message: CommonSendMessage): Promise<CommonSendMessage>;
sendPushMessage(targets: Target[], message: string): Promise<any>; sendPushMessage(targets: Target[], message: string): Promise<any>;
} }
@ -38,9 +40,9 @@ export class RobotManager {
async initialize() { async initialize() {
for (let file of fs.readdirSync(ROBOT_PATH)) { for (let file of fs.readdirSync(ROBOT_PATH)) {
let robotFile = `${ROBOT_PATH}/${file}`; let robotFile = `${ROBOT_PATH}/${file}`;
if (robotFile.match(/\.(js|mjs)$/)) { if (robotFile.match(/\.m?js$/)) {
// 加载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 { try {
let robotClass = require(robotFile)?.default; let robotClass = require(robotFile)?.default;
if (!robotClass) { if (!robotClass) {

@ -1,21 +1,27 @@
import { CommonGroupMessage, CommonReceivedMessage, CommonSendMessage } from "../message/Message"; import { CommonReceivedMessage } from "../message/Message";
import { GroupSender } from "../message/Sender"; import { PluginController } from "../PluginManager";
export class EchoController { export default class EchoController extends PluginController {
fliterBotGroupMessage(message: CommonGroupMessage) { public id = 'echo';
let newMsg = message.content.map((chunk) => { public name = '复读机';
if (chunk.type === 'text') { public description = '友好地复读消息';
return {
type: 'text', public autoSubscribe = true;
data: {
text: chunk.data.text.replace(/^[ ]*说[:, ]*/, '').replace(/我/g, '你'), public async initialize(): Promise<void> {
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 { } else {
return chunk; message.sendReply(repliedMessage, true);
}
} }
});
message.sendReply(newMsg);
} }
} }

@ -59,10 +59,27 @@ export class CommonMessage {
type: string | CommonMessageType = "text"; type: string | CommonMessageType = "text";
origin: string | CommonMessageOrigin = "private"; origin: string | CommonMessageOrigin = "private";
/** 回复的消息ID */ /** 回复的消息ID */
replyId?: string; repliedId?: string;
/** 提到的人 */ /** 提到的人 */
mentions?: { uid: string, text?: 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 * @param uid ID
@ -113,29 +130,99 @@ export class CommonMessage {
return true; 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 { export class CommonSendMessage extends CommonMessage {
/** 发送者 */
sender: Robot; sender: Robot;
/** 接收方的ID */
targetId: string; targetId: string;
constructor(sender: Robot, targetType: string, targetId: string, content?: MessageChunk[]) { /** 回复的消息 */
repliedMessage?: CommonReceivedMessage;
constructor(sender: Robot, origin: string, targetId: string, content?: MessageChunk[]) {
super(); super();
this.sender = sender; this.sender = sender;
this.type = targetType; this.origin = origin;
this.targetId = targetId; this.targetId = targetId;
if (Array.isArray(content)) this.content = content; if (Array.isArray(content)) this.content = content;
} }
} }
export class CommonReceivedMessage extends CommonMessage { export class CommonReceivedMessage extends CommonMessage {
// 接收时间 /** 接收时间 */
time: Date = new Date(); time: Date = new Date();
// 接收者 /** 接收者 */
receiver: Robot; receiver: Robot;
// 发送者 /** 发送者 */
sender: any; sender: any;
/** 接收者是否被提到 */
mentionedReceiver: boolean = false;
constructor(receiver: Robot, messageId?: string) { constructor(receiver: Robot, messageId?: string) {
super(); super();
@ -146,7 +233,7 @@ export class CommonReceivedMessage extends CommonMessage {
public async sendReply(message: string | MessageChunk[], addReply: boolean = false): Promise<CommonSendMessage | null> { public async sendReply(message: string | MessageChunk[], addReply: boolean = false): Promise<CommonSendMessage | null> {
const sender = this.sender as BaseSender; 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') { if (typeof message === 'string') {
let msgContent: MessageChunk[] = [{ let msgContent: MessageChunk[] = [{
type: 'text', type: 'text',
@ -159,6 +246,11 @@ export class CommonReceivedMessage extends CommonMessage {
return null; return null;
} }
if (addReply) {
newMessage.repliedId = this.id;
newMessage.repliedMessage = this;
}
newMessage = await this.receiver.sendMessage(newMessage); newMessage = await this.receiver.sendMessage(newMessage);
return newMessage; return newMessage;
@ -166,7 +258,8 @@ export class CommonReceivedMessage extends CommonMessage {
} }
export class CommonPrivateMessage<US extends UserSender> extends CommonReceivedMessage { export class CommonPrivateMessage<US extends UserSender> extends CommonReceivedMessage {
sender: US; public sender: US;
public origin = 'private';
constructor(sender: US, receiver: Robot, messageId?: string) { constructor(sender: US, receiver: Robot, messageId?: string) {
super(receiver, messageId); super(receiver, messageId);
@ -176,6 +269,7 @@ export class CommonPrivateMessage<US extends UserSender> extends CommonReceivedM
export class CommonGroupMessage<GS extends GroupSender = GroupSender> extends CommonReceivedMessage { export class CommonGroupMessage<GS extends GroupSender = GroupSender> extends CommonReceivedMessage {
sender: GS; sender: GS;
public origin = 'group';
constructor(sender: GS, receiver: Robot, messageId?: string) { constructor(sender: GS, receiver: Robot, messageId?: string) {
super(receiver, messageId); super(receiver, messageId);

@ -11,7 +11,7 @@ import { CommonReceivedMessage, CommonSendMessage, MentionMessage, TextMessage }
export type QQRobotConfig = { export type QQRobotConfig = {
user: string; user: string;
host: string; host: string;
baseId?: string; command_prefix?: string;
} }
export type QQGroupInfo = { export type QQGroupInfo = {
@ -27,6 +27,8 @@ export default class QQRobot implements Robot {
public uid: string; public uid: string;
public robotId: string; public robotId: string;
public commandPrefix: string[] = ['/', '', '!', ''];
private app: App; private app: App;
private endpoint: string; private endpoint: string;
@ -39,6 +41,14 @@ export default class QQRobot implements Robot {
this.robotId = robotId; this.robotId = robotId;
this.endpoint = 'http://' + config.host; this.endpoint = 'http://' + config.host;
this.uid = config.user; 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() { async initialize() {
@ -66,7 +76,7 @@ export default class QQRobot implements Robot {
} }
async initRestfulApi(router: RestfulRouter, api: RestfulApiManager) { 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) { async handlePostEvent(ctx: FullRestfulContext, next: koa.Next) {
@ -88,12 +98,19 @@ export default class QQRobot implements Robot {
* @param postData * @param postData
*/ */
async handleMessage(postData: any) { 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) { if (postData.message_id) {
let message: QQGroupMessage | QQPrivateMessage | undefined;
if (postData.message_type === 'group') { if (postData.message_type === 'group') {
// 处理群消息 // 处理群消息
let groupInfo = this.groupList.find((info) => info.groupId === postData.group_id); 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.groupInfo = groupInfo;
groupSender.groupName = groupInfo?.groupName; groupSender.groupName = groupInfo?.groupName;
groupSender.globalNickName = postData.sender?.nickname; groupSender.globalNickName = postData.sender?.nickname;
@ -102,21 +119,49 @@ export default class QQRobot implements Robot {
groupSender.level = postData.sender?.level; groupSender.level = postData.sender?.level;
groupSender.title = postData.sender?.title; 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.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') { } 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; 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.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') { if (message.origin === 'private') {
await this.sendToUser(message.targetId, msgData); await this.sendToUser(message.targetId, msgData);
} else if (message.origin === 'group') { } else if (message.origin === 'group') {
console.log('发送群消息', message.targetId, msgData);
await this.sendToGroup(message.targetId, msgData); await this.sendToGroup(message.targetId, msgData);
} }

@ -1,6 +1,7 @@
import TelegramBot from "node-telegram-bot-api"; import TelegramBot from "node-telegram-bot-api";
import App from "../App"; import App from "../App";
import { CommonSendMessage } from "../message/Message"; import { CommonSendMessage } from "../message/Message";
import { CommandInfo } from "../PluginManager";
import { Robot } from "../RobotManager"; import { Robot } from "../RobotManager";
import { Target } from "../SubscribeManager"; import { Target } from "../SubscribeManager";
import { Utils } from "../utils/Utils"; 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);
*/
}
/** /**
* *
*/ */

@ -1,6 +1,6 @@
import { CommonGroupMessage, CommonPrivateMessage, CommonReceivedMessage, CommonSendMessage, MentionMessage, MessageChunk, TextMessage } from "../../message/Message"; import { CommonGroupMessage, CommonPrivateMessage, CommonReceivedMessage, CommonSendMessage, MentionMessage, MessageChunk, TextMessage } from "../../message/Message";
import { GroupSender, UserSender } from "../../message/Sender"; import { GroupSender, UserSender } from "../../message/Sender";
import { QQGroupInfo } from "../QQRobot"; import QQRobot, { QQGroupInfo } from "../QQRobot";
export interface QQFaceMessage extends MessageChunk { export interface QQFaceMessage extends MessageChunk {
type: 'qqface'; type: 'qqface';
@ -66,7 +66,7 @@ export class QQGroupSender extends GroupSender {
* @param message * @param message
* @returns * @returns
*/ */
export async function parseQQMessageChunk(messageData: any[], message: CommonReceivedMessage): Promise<CommonReceivedMessage> { export async function parseQQMessageChunk(bot: QQRobot, messageData: any[], message: CommonReceivedMessage): Promise<CommonReceivedMessage> {
let willIgnoreMention = false; let willIgnoreMention = false;
messageData.forEach((chunkData) => { messageData.forEach((chunkData) => {
if (chunkData.type) { if (chunkData.type) {
@ -110,6 +110,9 @@ export async function parseQQMessageChunk(messageData: any[], message: CommonRec
case 'at': case 'at':
if (chunkData.data?.qq) { if (chunkData.data?.qq) {
if (!willIgnoreMention) { if (!willIgnoreMention) {
if (chunkData.data.qq == bot.uid) { // 如果是@机器人
message.mentionedReceiver = true;
} else { // @其他人的情况
message.mention(chunkData.data.qq); message.mention(chunkData.data.qq);
message.content.push({ message.content.push({
type: 'mention', type: 'mention',
@ -117,6 +120,7 @@ export async function parseQQMessageChunk(messageData: any[], message: CommonRec
uid: chunkData.data.qq uid: chunkData.data.qq
} }
} as MentionMessage); } as MentionMessage);
}
} else { } else {
willIgnoreMention = false; willIgnoreMention = false;
} }
@ -124,7 +128,7 @@ export async function parseQQMessageChunk(messageData: any[], message: CommonRec
break; break;
case 'reply': case 'reply':
if (chunkData.data?.id) { if (chunkData.data?.id) {
message.replyId = chunkData.data.id; message.repliedId = chunkData.data.id;
willIgnoreMention = true; // 忽略下一个“@” willIgnoreMention = true; // 忽略下一个“@”
} }
break; break;
@ -164,7 +168,7 @@ export async function convertMessageToQQChunk(message: CommonSendMessage) {
msgChunk.push({ msgChunk.push({
type: 'text', type: 'text',
data: { data: {
url: chunk.data.url text: chunk.data.text
} }
}); });
break; 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({ msgChunk.unshift({
type: 'reply', type: 'reply',
data: { id: message.replyId } data: { id: message.repliedId }
}); });
} }

Loading…
Cancel
Save