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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save