增加Reactive,修改消息后自动更新数据

main
落雨楓 1 year ago
parent ee3e03064d
commit 140b705324

@ -8,4 +8,8 @@ tpl:
default: |-
{{{postUser.displayname}}}「{{{postUser.username}}}」发布了帖子《{{{topic.titleRaw}}}》:
{{{excerpt post.content 100}}}
{{{topic.url}}}
# default: |-
# {{{postUser.displayname}}}「{{{postUser.username}}}」发布了帖子《{{{topic.titleRaw}}}》:
# {{{excerpt post.content 100}}}
# {{{topic.url}}}

@ -8,4 +8,5 @@ tpl:
default: |-
{{{postUser.displayname}}}「{{{postUser.username}}}」回复了 {{{topicUser.displayname}}}「{{{topicUser.username}}}」的帖子《{{topic.titleRaw}}》:
{{{excerpt post.content 100}}}
{{{post.url}}}
# {{{post.url}}}

@ -8,4 +8,3 @@ tpl:
default: |-
{{{author}}} 更新了页面《{{{title}}}》:
{{{summary}}}
{{{url}}}

@ -8,5 +8,3 @@ tpl:
default: |-
{{{author}}} 创建了新页面《{{{title}}}》:
{{{summary}}}
{{{url}}}

@ -52,5 +52,8 @@
"@types/yaml": "^1.9.7",
"ts-node": "^10.4.0",
"typescript": "^4.5.4"
},
"imports": {
"#ibot/*": "./dist/*"
}
}

@ -1,10 +1,11 @@
import App from "./App";
import { CommandOverrideConfig } from "./Config";
import { PermissionDeniedError, RateLimitError } from "./error/errors";
import { CommonReceivedMessage, CommonSendMessage } from "./message/Message";
import { CommonReceivedMessage } from "./message/Message";
import { ChatIdentity } from "./message/Sender";
import { CommandInfo, CommandInputArgs, EventScope, MessageEventOptions, MessagePriority, PluginEvent } from "./PluginManager";
import { Robot } from "./RobotManager";
import { Robot } from "./robot/Robot";
import { Reactive } from "./utils/reactive";
export type ControllerEventInfo = {
priority: number;
@ -143,7 +144,7 @@ export class EventManager {
public async emit(eventName: string, senderInfo?: ChatIdentity | null, ...args: any[]) {
if (this.app.debug) {
if (args[0] instanceof CommonReceivedMessage) {
if (typeof args[0] === 'object' && args[0].chatType) {
this.app.logger.debug(`触发事件 ${eventName} ${args[0].contentText}`);
} else {
this.app.logger.debug(`触发事件 ${eventName}`);
@ -166,8 +167,8 @@ export class EventManager {
console.error(error);
for (let arg of args) {
if (arg instanceof CommonReceivedMessage) {
const msg = arg;
if (typeof arg === 'object' && arg.chatType) {
const msg: CommonReceivedMessage = arg;
if (error instanceof RateLimitError) {
const retryAfterMinutes = Math.ceil(error.retryAfter / 60);
msg.sendReply(`使用太多了,${retryAfterMinutes}分钟后再试吧`);
@ -240,7 +241,7 @@ export class EventManager {
return isResolved;
}
public async emitMessage(message: CommonReceivedMessage) {
public async emitMessage(message: Reactive<CommonReceivedMessage>) {
let isResolved = false;
if (message.chatType === 'private' || (message.chatType === 'group' && message.mentionedReceiver)) {
@ -262,7 +263,7 @@ export class EventManager {
return false;
}
public async emitCommand(contentText: string, message: CommonReceivedMessage) {
public async emitCommand(contentText: string, message: Reactive<CommonReceivedMessage>) {
let command = '';
let param = '';
@ -309,7 +310,7 @@ export class EventManager {
return await this.emit(`raw/${robot.type}/${event}`, { type: 'raw', robot: robot }, event);
}
public async emitRawMessage(message: CommonReceivedMessage) {
public async emitRawMessage(message: Reactive<CommonReceivedMessage>) {
let isResolved = false;
await this.emit(`filter/message`, null, message);
@ -320,7 +321,7 @@ export class EventManager {
return await this.emit('raw/message', this.getSenderInfo(message), message);
}
public async emitFilterSendMessage(message: CommonSendMessage) {
public async emitFilterSendMessage(message: Reactive<CommonReceivedMessage>) {
}

@ -1,6 +1,5 @@
import { EventManager } from "./EventManager";
import { CommonReceivedMessage } from "./message/Message";
import { Robot } from "./RobotManager";
import fs from 'fs';
import fsAsync from 'fs/promises';
import chokidar from 'chokidar';
@ -10,6 +9,8 @@ import EventEmitter from "events";
import path from "path";
import { ChatIdentity } from "./message/Sender";
import { Utils } from "./utils/Utils";
import { Robot } from "./robot/Robot";
import { Reactive } from "./utils/reactive";
export const MessagePriority = {
LOWEST: 0,
@ -53,8 +54,8 @@ export type CommandInputArgs = {
param: string,
}
export type MessageCallback = (message: CommonReceivedMessage, resolved: VoidFunction) => any;
export type CommandCallback = (args: CommandInputArgs, message: CommonReceivedMessage, resolved: VoidFunction) => any;
export type MessageCallback = (message: Reactive<CommonReceivedMessage>, resolved: VoidFunction) => any;
export type CommandCallback = (args: CommandInputArgs, message: Reactive<CommonReceivedMessage>, resolved: VoidFunction) => any;
export type RawEventCallback = (robot: Robot, event: any, resolved: VoidFunction) => any;
export type AllowedList = string[] | '*';

@ -4,39 +4,9 @@ import path from "path";
import App from "./App";
import { MultipleMessage } from "./base/provider/BaseProvider";
import { RobotConfig } from "./Config";
import { CommonGroupMessage, CommonMessage, CommonPrivateMessage, CommonReceivedMessage, CommonSendMessage, MessageChunk } from "./message/Message";
import { GroupSender, ChatIdentity, UserSender, UserInfoType, GroupInfoType, RootGroupInfoType, ChannelInfoType, GroupUserInfoType } from "./message/Sender";
import { CommandInfo } from "./PluginManager";
import { RestfulApiManager, RestfulContext, RestfulRouter } from "./RestfulApiManager";
import { CacheStore } from "./CacheManager";
import { Target } from "./SubscribeManager";
import { MessageSchemaType } from "./odm/Message";
export interface Robot {
type: string;
robotId?: string;
userId?: string;
description?: string;
initialize?: () => Promise<any>;
destroy?: () => Promise<any>;
initRestfulApi?: (router: RestfulRouter, api: RestfulApiManager) => Promise<any>;
setCommands?(commands: CommandInfo[]): Promise<any>;
markRead?(message: CommonReceivedMessage): Promise<boolean>;
sendTyping?(chatIdentity: ChatIdentity): Promise<boolean>;
sendMessage(message: CommonSendMessage): Promise<CommonSendMessage>;
sendPushMessage(targets: Target[], message: string): Promise<any>;
deleteMessage?(chatIdentity: ChatIdentity, messageId: string): Promise<boolean>;
getSession(chatIdentity: ChatIdentity, type: string): CacheStore;
ensureMediaUrl?(mediaMessageChunk: MessageChunk): Promise<void>;
getUsersInfo?(userIds: string[]): Promise<(UserInfoType | null)[]>;
getGroupInfo?(groupId: string, rootGroupId?: string): Promise<GroupInfoType | null>;
getRootGroupInfo?(rootGroupId: string): Promise<RootGroupInfoType | null>;
getChannelInfo?(channelId: string): Promise<ChannelInfoType | null>;
getGroupUsersInfo?(userIds: string[], groupId: string, rootGroupId?: string): Promise<(GroupUserInfoType | null)[]>;
parseDBMessage?(dbMessage: MessageSchemaType): Promise<CommonMessage>;
}
import { CommonGroupMessage, CommonPrivateMessage, CommonReceivedMessage } from "./message/Message";
import { GroupSender, ChatIdentity, UserSender } from "./message/Sender";
import { Robot, RobotAdapter } from "./robot/Robot";
export class RobotManager {
private app: App;
@ -54,7 +24,7 @@ export class RobotManager {
}
public async initialize() {
const ROBOT_PATH = path.join(this.app.srcPath, "robot");
const ROBOT_PATH = path.join(this.app.srcPath, "robot/adapter");
for (let file of fs.readdirSync(ROBOT_PATH)) {
let robotFile = `${ROBOT_PATH}/${file}`;
@ -84,12 +54,12 @@ export class RobotManager {
if (robotType in this.robotClasses) {
let robotClass = this.robotClasses[robotType];
try {
let robotObject: Robot = new robotClass(this.app, robotId, robotConfig);
let robotAdapter: RobotAdapter = new robotClass(this.app, robotId, robotConfig);
let robotWrapper: Robot = new Robot(this.app, robotId, robotAdapter);
this.robots[robotId] = robotObject;
this.robots[robotId] = robotWrapper;
await robotObject.initialize?.();
await robotObject.initRestfulApi?.(this.app.restfulApi.getRobotRouter(robotId), this.app.restfulApi);
await robotWrapper.initialize();
this.app.logger.info(`已加载Robot: ${robotId}`);
} catch(err) {

@ -1,22 +1,42 @@
import { AuthType, createClient } from "webdav";
import App from "../App";
import { extname } from "path";
import { AttachmentMessage } from "../message/Message";
import { CommonReceivedMessage } from "../message/Message";
import { MessagePriority, PluginController, PluginEvent } from "../PluginManager";
import got from "got/dist/source";
import { RandomMessage } from "../utils/RandomMessage";
import { QQForwardingMessage } from "../robot/qq/Message";
import QQRobot from "../robot/QQRobot";
import { ChatIdentity, UserSender } from "../message/Sender";
import { Utils } from "../utils/Utils";
import { MessageUtils } from "../utils/message";
import { QQForwardingMessage } from "../robot/adapter/qq/Message";
import QQRobot from "../robot/adapter/QQRobot";
import { GroupSender } from "../message/Sender";
import { Robot } from "#ibot/robot/Robot";
export type IsekaiBBSQuicklyPostConfig = {
api_endpoint: string,
token: string,
};
export type IsekaiQuicklyPostMessageData = {
/** 发布账号 */
account: string,
/** 发布账号的昵称QQ昵称、邮箱姓名 */
nickname?: string,
/** 头像 */
avatar?: string,
/** 文章内容Markdown */
content: string,
/** 消息ID用于建立回复树 */
id?: string,
/** 回复的消息ID */
replyId?: string,
}
export type IsekaiQuicklyPostBody = {
/** 发布来源 */
srcType: string,
/** 文章标题 */
title?: string,
/** 消息列表 */
messages: IsekaiQuicklyPostMessageData[],
};
export default class IsekaiBBSQuicklyPost implements PluginController {
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
@ -101,48 +121,87 @@ export default class IsekaiBBSQuicklyPost implements PluginController {
let markdownBuilder: string[] = [];
message.content.forEach(messageChunk => {
if (messageChunk.type.includes('text')) {
markdownBuilder.push(messageChunk.data?.text ?? '');
markdownBuilder.push(messageChunk.text ?? '');
} else if (messageChunk.type.includes('image')) {
markdownBuilder.push(`![${messageChunk.data?.alt ?? ''}](${messageChunk.data?.url ?? ''})`);
} else if (messageChunk.type.includes('emoji')) {
markdownBuilder.push(messageChunk.text ?? '🫥');
} else if (messageChunk.type.includes('record')) {
markdownBuilder.push('[语音消息]');
} else if (messageChunk.type.includes('mention')) {
if (messageChunk.data?.text) {
markdownBuilder.push(`&#64;${messageChunk.data.text}`);
markdownBuilder.push(`**&#64;${messageChunk.data.text}**`);
}
}
});
return markdownBuilder.join('');
}
async postNewThread(message: CommonReceivedMessage, groupConfig: IsekaiBBSQuicklyPostConfig) {
if (message.receiver.type !== 'qq') {
async postNewThread(refMessage: CommonReceivedMessage, groupConfig: IsekaiBBSQuicklyPostConfig) {
if (refMessage.receiver.type !== 'qq') {
// TODO: support other platform
return;
}
let attachmentMsg = message.content[0] as QQForwardingMessage;
let attachmentMsg = refMessage.content[0] as QQForwardingMessage;
let resId = attachmentMsg.data.res_id;
let robot = message.receiver as QQRobot;
let robot = refMessage.receiver as Robot<QQRobot>;
let referenceSender = refMessage.sender as GroupSender;
message.markRead()
refMessage.markRead()
this.app.logger.info(`[群号:${message.sender.groupId}] 收到合并转发消息正在发送到BBS。`);
this.app.logger.info(`[群号:${refMessage.sender.groupId}] 收到合并转发消息 ${resId}正在发送到BBS。`);
let messageList = await robot.getReferencedMessages(resId);
let messageList = await robot.adapter.getReferencedMessages(resId);
if (!messageList || messageList.length === 0) {
this.app.logger.info(`[群号:${message.sender.groupId}] 合并转发消息内容为空或无法获取。`);
this.app.logger.info(`[群号:${refMessage.sender.groupId}] 合并转发消息内容为空或无法获取。`);
return;
}
try {
let markdownBuilder = [];
let markdownBuilder: string[] = [];
for (let message of messageList) {
const sender = message.sender as GroupSender;
const content = await this.messageToMarkdown(message);
markdownBuilder.push('**' + (sender.displayName ?? sender.userName ?? sender.userId) + ':** ');
markdownBuilder.push(content);
markdownBuilder.push('\n');
}
let postData = {
srcType: 'qq',
messages: [{
account: referenceSender.userId,
nickname: referenceSender.displayName,
avatar: robot.adapter.infoProvider.getUserImage(referenceSender.userId),
content: markdownBuilder.join('\n'),
}],
} as IsekaiQuicklyPostBody;
const res = await got.post(groupConfig.api_endpoint, {
json: postData,
headers: {
authorization: `Bearer ${groupConfig.token}`,
}
}).json<any>();
if (res.error) {
throw new Error(res.message);
}
// 保存threadId到消息
refMessage.extra['isekai_bbs_quicklypost'] = {
threadId: res.tid,
};
} catch(err: any) {
this.app.logger.error("转存群文件失败:" + err.message, err);
this.app.logger.error("转发消息到BBS失败:" + err.message, err);
console.error(err);
let msg = this.messageGroup.error.nextMessage(err.message);
await message.sendReply(msg ?? `转存群文件失败:${err.message}`, false);
let msg = this.messageGroup.error.nextMessage({
error: err.message,
});
await refMessage.sendReply(msg ?? `转发失败:${err.message}`, false);
}
}
}

@ -1,5 +1,5 @@
import { Robot } from "#ibot/robot/Robot";
import App from "../App";
import { Robot } from "../RobotManager";
export class ChatThread {
private app: App;

@ -1,9 +1,10 @@
import { Robot } from "../RobotManager";
import { CacheStore } from "../CacheManager";
import { MessageDataType, MessageSchemaType, chatIdentityToDB } from "../odm/Message";
import { MessageDataType, chatIdentityToDB } from "../odm/Message";
import { BaseSender, ChatIdentity, GroupSender, IMessageSender, UserSender } from "./Sender";
import { LiteralUnion } from "../utils/types";
import { Utils } from "../utils/Utils";
import { Robot } from "#ibot/robot/Robot";
import { Reactive, reactive } from "#ibot/utils/reactive";
export enum MessageDirection {
RECEIVE = 1,
@ -85,7 +86,7 @@ export class CommonMessage {
deleted: boolean = false;
/** 附加信息 */
extra: any = {};
extra: any = reactive({});
private _contentText?: string;
@ -234,7 +235,7 @@ export class CommonSendMessage extends CommonMessage {
receiver: ChatIdentity;
/** 回复的消息 */
repliedMessage?: CommonReceivedMessage;
repliedMessage?: Reactive<CommonReceivedMessage>;
/** 发送时间 */
time: Date = new Date();
@ -290,6 +291,10 @@ export class CommonReceivedMessage extends CommonMessage {
/** Session存储 */
session: SessionStoreGroup = new Proxy({} as any, {
get: (target, p) => {
if (p.toString().startsWith('_')) {
return undefined;
}
if (!target[p]) {
target[p] = this.getSession(p as string);
}
@ -321,7 +326,6 @@ export class CommonReceivedMessage extends CommonMessage {
if (addReply) {
newMessage.repliedId = this.id;
newMessage.repliedMessage = this;
}
return newMessage;
@ -348,10 +352,10 @@ export class CommonReceivedMessage extends CommonMessage {
let newMessage = this.createReplyMessage(message, shouldReply);
if (newMessage.content.length === 0) return null;
newMessage.extra = {
newMessage.extra = reactive({
...newMessage.extra,
...extra,
};
});
newMessage = await this.receiver.sendMessage(newMessage);

@ -1,5 +1,6 @@
import { Robot } from "#ibot/robot/Robot";
import { LiteralUnion } from "#ibot/utils/types";
import { any } from "micromatch";
import { Robot } from "../RobotManager";
export type BaseSenderType = "user" | "group" | "channel";
@ -92,11 +93,11 @@ export class GroupSender {
}
get groupDisplayName() {
return this.groupName ?? this.groupId;
return this.groupName || this.groupId;
}
get displayName() {
return this.nickName ?? this.globalNickName ?? this.userName ?? this.userId;
return this.nickName || this.globalNickName || this.userName || this.userId;
}
get userSender() {
@ -109,7 +110,7 @@ export class GroupSender {
}
export interface ChatIdentity {
type: ('private' | 'group' | 'channel' | 'raw') | (string & {});
type: LiteralUnion<'private' | 'group' | 'channel' | 'raw'>;
robot: Robot;
rootGroupId?: string;
groupId?: string;

@ -0,0 +1,275 @@
import { Reactive, reactive } from "#ibot/utils/reactive";
import App from "../App";
import { CacheStore } from "../CacheManager";
import { CommandInfo } from "../PluginManager";
import { RestfulRouter } from "../RestfulApiManager";
import { Target } from "../SubscribeManager";
import { CommonReceivedMessage, CommonSendMessage, MessageChunk, CommonMessage, MessageDirection } from "../message/Message";
import { ChatIdentity, UserInfoType, GroupInfoType, RootGroupInfoType, ChannelInfoType, GroupUserInfoType, UserSender, BaseSender, GroupSender } from "../message/Sender";
import { MessageDataType, MessageSchemaType } from "../odm/Message";
import { RobotStorage } from "../storage/RobotStorage";
export interface RobotAdapter {
type: string;
robotId?: string;
userId?: string;
description?: string;
initialize?: (wrapper: Robot<RobotAdapter>) => Promise<any>;
destroy?: () => Promise<any>;
setCommands?(commands: CommandInfo[]): Promise<any>;
markRead?(message: CommonReceivedMessage): Promise<boolean>;
sendTyping?(chatIdentity: ChatIdentity, typing: boolean): Promise<boolean>;
sendMessage(message: CommonSendMessage): Promise<CommonSendMessage>;
sendPushMessage(targets: Target[], message: string): Promise<any>;
deleteMessage?(chatIdentity: ChatIdentity, messageId: string): Promise<boolean>;
retrieveMediaUrl?(mediaMessageChunk: MessageChunk): Promise<void>;
getUsersInfo?(userIds: string[]): Promise<(UserInfoType | null)[]>;
getGroupInfo?(groupId: string, rootGroupId?: string): Promise<GroupInfoType | null>;
getRootGroupInfo?(rootGroupId: string): Promise<RootGroupInfoType | null>;
getChannelInfo?(channelId: string): Promise<ChannelInfoType | null>;
getGroupUsersInfo?(userIds: string[], groupId: string, rootGroupId?: string): Promise<(GroupUserInfoType | null)[]>;
parseDBMessage?(dbMessage: MessageSchemaType): Promise<CommonMessage>;
}
export class Robot<Adapter extends RobotAdapter = any> {
public type: string;
public robotId: string;
public description: string = '';
// 机器人配置项
public commandPrefix: string[] = ['/'];
public account: string = '';
public adapter: Adapter;
public storages?: RobotStorage;
public restfulRouter!: RestfulRouter;
private app: App;
constructor(app: App, robotId: string, adapter: Adapter) {
this.app = app;
this.robotId = robotId;
this.adapter = adapter;
this.type = adapter.type;
this.description = adapter.description || '';
}
async initialize() {
// Restful API
this.restfulRouter = this.app.restfulApi.getRobotRouter(this.robotId);
// Storages
this.storages = await this.app.storage.getStorages(this.robotId);
await this.adapter.initialize?.(this);
}
async destroy() {
await this.adapter.destroy?.();
}
public getSession(chatIdentity: ChatIdentity, type: string) {
const sessionPath = this.app.robot.getSessionPath(chatIdentity, type);
return this.app.cache.getStore(sessionPath);
}
/**
*
* @param commands
* @returns
*/
public setCommands(commands: CommandInfo[]): Promise<any> {
return this.adapter.setCommands ? this.adapter.setCommands(commands) : Promise.resolve();
}
/**
*
* @param message
* @returns
*/
public markRead(message: CommonReceivedMessage): Promise<boolean> {
return this.adapter.markRead ? this.adapter.markRead(message) : Promise.resolve(false);
}
/**
*
* @param chatIdentity
* @param typing
* @returns
*/
public sendTyping(chatIdentity: ChatIdentity, typing: boolean): Promise<boolean> {
return this.adapter.sendTyping ? this.adapter.sendTyping(chatIdentity, typing) : Promise.resolve(false);
}
/**
*
* @param chatIdentity
* @param callback
*/
async wrapTyping(chatIdentity: ChatIdentity, callback: () => Promise<any>): Promise<any> {
let timer = setInterval(() => {
this.sendTyping(chatIdentity, true);
});
this.sendTyping(chatIdentity, true);
try {
await callback();
clearInterval(timer);
this.sendTyping(chatIdentity, false);
} catch(err) {
clearInterval(timer);
this.sendTyping(chatIdentity, false);
throw err;
}
}
/**
*
* @param message
* @returns
*/
public sendMessage(message: CommonSendMessage): Promise<CommonSendMessage> {
return this.adapter.sendMessage ? this.adapter.sendMessage(message) : Promise.resolve(message);
}
public sendPushMessage(targets: Target[], message: string): Promise<any> {
return this.adapter.sendPushMessage ? this.adapter.sendPushMessage(targets, message) : Promise.resolve();
}
/**
*
* @param chatIdentity
* @param messageId
* @returns
*/
public deleteMessage(chatIdentity: ChatIdentity, messageId: string): Promise<boolean> {
return this.adapter.deleteMessage ? this.adapter.deleteMessage(chatIdentity, messageId) : Promise.resolve(false);
}
/**
* URL
* @param mediaMessageChunk
* @returns
*/
public retrieveMediaUrl(mediaMessageChunk: MessageChunk): Promise<void> {
return this.adapter.retrieveMediaUrl ? this.adapter.retrieveMediaUrl(mediaMessageChunk) : Promise.resolve();
}
/**
*
* @param dbMessage
* @returns
*/
public async parseDBMessage(dbMessage: MessageSchemaType): Promise<Reactive<CommonMessage> | null> {
let parsedMessage: CommonMessage | null = null;
if (this.adapter.parseDBMessage) {
parsedMessage = await this.adapter.parseDBMessage(dbMessage);
} else {
const chatIdentity: ChatIdentity = {
...dbMessage.chatIdentity,
robot: this,
type: dbMessage.chatType,
};
if (dbMessage.direction === MessageDirection.RECEIVE) {
let sender: UserSender | GroupSender | null = null;
if (dbMessage.chatType === 'private' && dbMessage.chatIdentity.userId) {
sender = new UserSender(this, dbMessage.chatIdentity.userId);
let userInfo = await this.storages?.userInfo.get(dbMessage.chatIdentity.userId);
if (userInfo) {
sender.userName = userInfo.userName;
sender.nickName = userInfo.nickName;
}
} else if (dbMessage.chatType === 'group' && dbMessage.chatIdentity.userId && dbMessage.chatIdentity.groupId) {
sender = new GroupSender(this, dbMessage.chatIdentity.groupId, dbMessage.chatIdentity.userId);
let userInfo = await this.storages?.userInfo.get(dbMessage.chatIdentity.userId);
let rootGroupInfo = dbMessage.chatIdentity.rootGroupId ?
await this.storages?.rootGroupInfo.get(dbMessage.chatIdentity.rootGroupId) :
null;
let groupInfo = await this.storages?.groupInfo.get(dbMessage.chatIdentity.groupId, dbMessage.chatIdentity.rootGroupId);
let groupUserInfo = await this.storages?.groupUserInfo.get(dbMessage.chatIdentity.userId, dbMessage.chatIdentity.groupId);
if (userInfo) {
sender.userName = userInfo.userName;
sender.globalNickName = userInfo.nickName;
}
if (rootGroupInfo) {
sender.rootGroupName = rootGroupInfo.name;
}
if (groupInfo) {
sender.groupName = groupInfo.name;
}
if (groupUserInfo) {
sender.userName = groupUserInfo.userName;
sender.nickName = groupUserInfo.nickName;
}
}
if (sender) {
let message = new CommonReceivedMessage(this, sender, dbMessage.messageId);
message.content = dbMessage.content;
message.repliedId = dbMessage.repliedMessageId;
message.mentions = dbMessage.mentionedUserIds?.map((userId) => {
return {
userId,
};
}) ?? [];
message.time = dbMessage.time;
}
} else if (dbMessage.direction === MessageDirection.SEND) {
let message = new CommonSendMessage(this, dbMessage.chatType, chatIdentity, dbMessage.content);
message.id = dbMessage.messageId;
message.repliedId = dbMessage.repliedMessageId;
message.mentions = dbMessage.mentionedUserIds?.map((userId) => {
return {
userId,
};
}) ?? [];
message.time = dbMessage.time;
message.extra = reactive(dbMessage.extra);
parsedMessage = message;
}
}
if (parsedMessage) {
if (this.storages) {
return this.storages.message.reactive(parsedMessage);
} else {
return reactive(parsedMessage);
}
}
return null;
}
getUsersInfo(userIds: string[]): Promise<(UserInfoType | null)[]> {
return this.adapter.getUsersInfo ? this.adapter.getUsersInfo(userIds) : Promise.resolve([]);
}
getGroupInfo(groupId: string, rootGroupId?: string): Promise<GroupInfoType | null> {
return this.adapter.getGroupInfo ? this.adapter.getGroupInfo(groupId, rootGroupId) : Promise.resolve(null);
}
getRootGroupInfo(rootGroupId: string): Promise<RootGroupInfoType | null> {
return this.adapter.getRootGroupInfo ? this.adapter.getRootGroupInfo(rootGroupId) : Promise.resolve(null);
}
getChannelInfo(channelId: string): Promise<ChannelInfoType | null> {
return this.adapter.getChannelInfo ? this.adapter.getChannelInfo(channelId) : Promise.resolve(null);
}
getGroupUsersInfo(userIds: string[], groupId: string, rootGroupId?: string): Promise<(GroupUserInfoType | null)[]> {
return this.adapter.getGroupUsersInfo ? this.adapter.getGroupUsersInfo(userIds, groupId, rootGroupId) : Promise.resolve([]);
}
}

@ -1,17 +1,16 @@
import koa from "koa";
import got from "got/dist/source";
import fs from "fs";
import App from "../App";
import { Robot } from "../RobotManager";
import { Target } from "../SubscribeManager";
import { Utils } from "../utils/Utils";
import { FullRestfulContext, RestfulApiManager, RestfulRouter } from "../RestfulApiManager";
import App from "../../App";
import { Robot, RobotAdapter } from "../Robot";
import { Target } from "../../SubscribeManager";
import { Utils } from "../../utils/Utils";
import { FullRestfulContext, RestfulApiManager, RestfulRouter } from "../../RestfulApiManager";
import { convertMessageToQQChunk, parseQQMessageChunk, QQAttachmentMessage, QQGroupMessage, QQGroupSender, QQPrivateMessage, QQUserSender } from "./qq/Message";
import { CommonReceivedMessage, CommonSendMessage, MessageChunk } from "../message/Message";
import { PluginController } from "../PluginManager";
import { RobotConfig } from "../Config";
import { ChatIdentity, GroupInfoType, UserInfoType } from "../message/Sender";
import { CommonReceivedMessage, CommonSendMessage, MessageChunk } from "../../message/Message";
import { PluginController } from "../../PluginManager";
import { RobotConfig } from "../../Config";
import { ChatIdentity } from "../../message/Sender";
import { QQInfoProvider } from "./qq/InfoProvider";
export type QQRobotConfig = RobotConfig & {
@ -27,18 +26,19 @@ export type QQGroupInfo = {
memberLimit?: number
};
export default class QQRobot implements Robot {
export default class QQRobot implements RobotAdapter {
public type = 'qq';
public userId: string;
public robotId: string;
public description: string;
public commandPrefix: string[] = ['/', '', '!', ''];
public infoProvider: QQInfoProvider;
private config: QQRobotConfig;
private app: App;
private wrapper!: Robot<QQRobot>;
private endpoint: string;
private taskId?: NodeJS.Timer;
@ -49,24 +49,30 @@ export default class QQRobot implements Robot {
constructor(app: App, robotId: string, config: QQRobotConfig) {
this.app = app;
this.robotId = robotId;
this.config = config;
this.endpoint = 'http://' + config.host;
this.userId = config.userId.toString();
this.description = config.description ?? this.app.config.robot_description ?? 'Isekai Feedbot for QQ';
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];
}
}
this.messageTypeHandler.help = this.parseHelpMessage.bind(this);
this.infoProvider = new QQInfoProvider(app, this, config);
}
async initialize() {
async initialize(wrapper: Robot) {
this.wrapper = wrapper;
if (this.config.command_prefix) {
if (Array.isArray(this.config.command_prefix)) {
this.wrapper.commandPrefix = this.config.command_prefix;
} else if (typeof this.config.command_prefix === 'string') {
this.wrapper.commandPrefix = [this.config.command_prefix];
}
}
this.wrapper.account = this.userId;
await this.initRestfulApi(this.wrapper.restfulRouter);
await this.infoProvider.initialize();
}
@ -74,7 +80,7 @@ export default class QQRobot implements Robot {
await this.infoProvider.destroy();
}
async initRestfulApi(router: RestfulRouter, api: RestfulApiManager) {
async initRestfulApi(router: RestfulRouter) {
router.post(`/event`, this.handlePostEvent.bind(this));
}
@ -116,10 +122,10 @@ export default class QQRobot implements Robot {
}
helpBuilder.push(
'可用的指令前缀:"' + this.commandPrefix.join('"、"') + '"',
'可用的指令前缀:"' + this.wrapper.commandPrefix.join('"、"') + '"',
'功能列表:'
);
const mainCommandPrefix = this.commandPrefix[0];
const mainCommandPrefix = this.wrapper.commandPrefix[0];
for (let controller of controllers) {
helpBuilder.push(`${controller.name}`);
@ -151,7 +157,7 @@ export default class QQRobot implements Robot {
async handleMessage(postData: any) {
let isResolved = false;
if (postData.type) {
isResolved = await this.app.event.emitRawEvent(this, postData.type, postData);
isResolved = await this.app.event.emitRawEvent(this.wrapper, postData.type, postData);
if (isResolved) return;
}
@ -161,7 +167,7 @@ export default class QQRobot implements Robot {
// 处理群消息
let groupInfo = this.infoProvider.groupList.find((info) => info.groupId === postData.group_id);
let groupSender = new QQGroupSender(this, postData.group_id.toString(), postData.user_id.toString());
let groupSender = new QQGroupSender(this.wrapper, postData.group_id.toString(), postData.user_id.toString());
groupSender.groupInfo = groupInfo;
groupSender.groupName = groupInfo?.groupName;
groupSender.globalNickName = postData.sender?.nickname;
@ -170,7 +176,7 @@ export default class QQRobot implements Robot {
groupSender.level = postData.sender?.level;
groupSender.title = postData.sender?.title;
message = new QQGroupMessage(groupSender, this, postData.message_id.toString());
message = new QQGroupMessage(groupSender, this.wrapper, postData.message_id.toString());
message.time = new Date(postData.time * 1000);
message = await parseQQMessageChunk(this, postData.message ?? [], message);
@ -179,10 +185,10 @@ export default class QQRobot implements Robot {
await this.infoProvider.updateUserSender(groupSender.userSender);
} else if (postData.message_type === 'private') {
// 处理私聊消息
let userSender = new QQUserSender(this, postData.user_id.toString());
let userSender = new QQUserSender(this.wrapper, postData.user_id.toString());
userSender.nickName = postData.sender?.nickname;
message = new QQPrivateMessage(userSender, this, postData.message_id.toString());
message = new QQPrivateMessage(userSender, this.wrapper, postData.message_id.toString());
message.time = new Date(postData.time * 1000);
message = await parseQQMessageChunk(this, postData.message ?? [], message);
@ -192,21 +198,21 @@ export default class QQRobot implements Robot {
if (message) {
// 保存消息
this.infoProvider.saveMessage(message);
let messageRef = this.infoProvider.saveMessage(message);
// 处理原始消息
isResolved = await this.app.event.emitRawMessage(message);
isResolved = await this.app.event.emitRawMessage(messageRef);
if (isResolved) return;
// 处理指令
let commandText = this.getCommandContentText(message);
let commandText = this.getCommandContentText(messageRef);
if (commandText) {
await this.app.event.emitCommand(commandText, message);
await this.app.event.emitCommand(commandText, messageRef);
return;
}
// 处理消息
isResolved = await this.app.event.emitMessage(message);
isResolved = await this.app.event.emitMessage(messageRef);
if (isResolved) return;
}
}
@ -221,11 +227,11 @@ export default class QQRobot implements Robot {
// 处理群消息
let groupInfo = this.infoProvider.groupList.find((info) => info.groupId === postData.group_id);
let groupSender = new QQGroupSender(this, postData.group_id.toString(), postData.user_id.toString());
let groupSender = new QQGroupSender(this.wrapper, postData.group_id.toString(), postData.user_id.toString());
groupSender.groupInfo = groupInfo;
groupSender.groupName = groupInfo?.groupName;
let message = new QQGroupMessage(groupSender, this);
let message = new QQGroupMessage(groupSender, this.wrapper);
message.time = new Date(postData.time * 1000);
message.type = 'attachment';
@ -242,18 +248,20 @@ export default class QQRobot implements Robot {
}
} as QQAttachmentMessage);
let messageRef = await this.infoProvider.saveMessage(message);
let isResolved = false;
// 处理原始消息
isResolved = await this.app.event.emitRawMessage(message);
isResolved = await this.app.event.emitRawMessage(messageRef);
if (isResolved) return;
// 处理消息
isResolved = await this.app.event.emitMessage(message);
isResolved = await this.app.event.emitMessage(messageRef);
if (isResolved) return;
}
getCommandContentText(message: CommonReceivedMessage) {
for (let prefix of this.commandPrefix) {
for (let prefix of this.wrapper.commandPrefix) {
if (message.contentText.startsWith(prefix)) {
// 移除指令前缀
if (message.content[0].data?.text) {
@ -265,12 +273,7 @@ export default class QQRobot implements Robot {
return null;
}
getSession(chatIdentity: ChatIdentity, type: string) {
const sessionPath = this.app.robot.getSessionPath(chatIdentity, type);
return this.app.cache.getStore(sessionPath);
}
async ensureMediaUrl(mediaMessageChunk: MessageChunk): Promise<void> {
async retrieveMediaUrl(mediaMessageChunk: MessageChunk): Promise<void> {
if (!mediaMessageChunk.data.url) {
if (mediaMessageChunk.type.includes('qqattachment')) {
let data = mediaMessageChunk.data;
@ -298,22 +301,34 @@ export default class QQRobot implements Robot {
*
*/
async getReferencedMessages(resId: string): Promise<CommonReceivedMessage[] | null> {
const res = await this.callRobotApi('/get_forward_msg', {
const res = await this.callRobotApi('get_forward_msg', {
message_id: resId,
});
if (!Array.isArray(res?.messages)) {
if (!Array.isArray(res?.data?.messages)) {
return null;
}
let messageList: CommonReceivedMessage[] = [];
for (let messageData of res.messages) {
for (let messageData of res.data.messages) {
if (messageData) {
let userSender = new QQUserSender(this, messageData.sender?.user_id.toString());
messageData.content ??= [];
let userSender = new QQUserSender(this.wrapper, messageData.sender?.user_id.toString());
userSender.nickName = messageData.sender?.nickname;
let message = new QQPrivateMessage(userSender, this);
let message = new QQPrivateMessage(userSender, this.wrapper);
// 生成消息ID
message.id = `ref:${userSender.userId}:${messageData.time}`;
message.time = new Date(messageData.time * 1000);
// 修改回复消息的指向
messageData.content.forEach((chunk: any) => {
if (chunk?.type === 'reply' && chunk.data?.qq && chunk.data?.time) {
chunk.data.id = `ref:${chunk.data.qq}:${chunk.data.time}`;
}
})
message = await parseQQMessageChunk(this, messageData.content ?? [], message);
messageList.push(message);

@ -1,19 +1,19 @@
import TelegramBot from "node-telegram-bot-api";
import App from "../App";
import { RobotConfig } from "../Config";
import { CommonSendMessage } from "../message/Message";
import { ChatIdentity } from "../message/Sender";
import { CommandInfo } from "../PluginManager";
import { Robot } from "../RobotManager";
import { Target } from "../SubscribeManager";
import { Utils } from "../utils/Utils";
import App from "../../App";
import { RobotConfig } from "../../Config";
import { CommonSendMessage } from "../../message/Message";
import { ChatIdentity } from "../../message/Sender";
import { CommandInfo } from "../../PluginManager";
import { Target } from "../../SubscribeManager";
import { Utils } from "../../utils/Utils";
import { RobotAdapter } from "../Robot";
export type TelegramRobotConfig = RobotConfig & {
token: string;
proxy?: string;
}
export default class TelegramRobot implements Robot {
export default class TelegramRobot implements RobotAdapter {
private app: App;
public type = 'telegram';
@ -77,11 +77,6 @@ export default class TelegramRobot implements Robot {
*/
}
getSession(chatIdentity: ChatIdentity, type: string) {
const sessionPath = this.app.robot.getSessionPath(chatIdentity, type);
return this.app.cache.getStore(sessionPath);
}
/**
*
*/

@ -1,10 +1,11 @@
import App from "../../App";
import { compareProps } from "../../utils/func";
import QQRobot, { QQRobotConfig } from "../QQRobot";
import App from "../../../App";
import { compareProps } from "../../../utils/func";
import { QQGroupSender, QQUserSender } from "./Message";
import { GroupInfoType, GroupUserInfoType, UserInfoType } from "../../message/Sender";
import { CommonMessage } from "../../message/Message";
import { RobotStorage } from "../../storage/RobotStorage";
import { GroupInfoType, GroupUserInfoType, UserInfoType } from "../../../message/Sender";
import { CommonMessage } from "../../../message/Message";
import { RobotStorage } from "../../../storage/RobotStorage";
import { Reactive, reactive } from "../../../utils/reactive";
import QQRobot, { QQRobotConfig } from "../QQRobot";
export type QQGroupInfo = {
groupId: string,
@ -79,11 +80,17 @@ export class QQInfoProvider {
});
}
public saveMessage(message: CommonMessage) {
this.storages?.message.set(message).catch((err: any) => {
public saveMessage<T extends CommonMessage>(message: T): Reactive<T> {
if (this.storages) {
this.storages.message.set(message).catch((err: any) => {
this.app.logger.error(`将消息保存到数据库出错: ${err.message}`);
console.error(err);
})
});
return this.storages.message.reactive(message);
} else {
return reactive(message);
}
}
async getGroupList(): Promise<any[]> {
@ -152,7 +159,7 @@ export class QQInfoProvider {
* @param userId
* @returns
*/
private getUserImage(userId: string) {
public getUserImage(userId: string) {
return `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`;
}
@ -161,7 +168,7 @@ export class QQInfoProvider {
* @param groupId
* @returns
*/
private getGroupImage(groupId: string) {
public getGroupImage(groupId: string) {
return `https://p.qlogo.cn/gh/${groupId}/${groupId}/100`
}

@ -1,5 +1,18 @@
import { AttachmentMessage, CommonGroupMessage, CommonPrivateMessage, CommonReceivedMessage, CommonSendMessage, EmojiMessage, ImageMessage, MentionMessage, MessageChunk, TextMessage, RecordMessage } from "../../message/Message";
import { GroupSender, UserSender } from "../../message/Sender";
import { Robot } from "#ibot/robot/Robot";
import {
AttachmentMessage,
CommonGroupMessage,
CommonPrivateMessage,
CommonReceivedMessage,
CommonSendMessage,
EmojiMessage,
ImageMessage,
MentionMessage,
MessageChunk,
TextMessage,
RecordMessage
} from "../../../message/Message";
import { GroupSender, UserSender } from "../../../message/Sender";
import QQRobot, { QQGroupInfo } from "../QQRobot";
import { qqFaceToEmoji } from "./emojiMap";
@ -51,14 +64,14 @@ export interface QQAttachmentMessage extends AttachmentMessage {
}
export interface QQForwardingMessage extends MessageChunk {
type: ['qqforwarding'];
type: ['reference', 'qqforwarding'];
data: {
res_id: string;
}
}
export class QQUserSender extends UserSender {
constructor(robot: QQRobot, userId: string) {
constructor(robot: Robot<QQRobot>, userId: string) {
super(robot, userId);
this.userName = userId;
}
@ -70,7 +83,7 @@ export class QQGroupSender extends GroupSender {
public title?: string;
public groupInfo?: QQGroupInfo;
constructor(robot: QQRobot, groupId: string, userId: string) {
constructor(robot: Robot<QQRobot>, groupId: string, userId: string) {
super(robot, groupId, userId);
this.userName = userId;
}
@ -176,13 +189,54 @@ export async function parseQQMessageChunk(bot: QQRobot, messageData: any[], mess
console.log('forwarding message', chunkData.data.data);
if (jsonData.meta?.detail?.resid) {
message.content.push({
type: ['qqforwarding'],
type: ['reference', 'qqforwarding'],
text: '[合并转发消息]',
data: {
res_id: jsonData.meta.detail.resid
}
} as QQForwardingMessage);
}
break;
case 'com.tencent.miniapp_01':
if (jsonData.meta?.detail_1?.qqdocurl) {
message.content.push({
type: ['text', 'qqurl'],
text: jsonData.meta.detail_1.qqdocurl,
data: {
url: jsonData.meta.detail_1.qqdocurl,
title: jsonData.meta.detail_1.desc,
}
} as QQUrlMessage);
} else if (jsonData.meta?.detail_1?.url) {
message.content.push({
type: ['text', 'qqurl'],
text: jsonData.meta.detail_1.url,
data: {
url: jsonData.meta.detail_1.url,
title: jsonData.meta.detail_1.desc,
}
} as QQUrlMessage);
}
break;
case 'com.tencent.structmsg':
if (jsonData.meta) {
for (let item of Object.values<any>(jsonData.meta)) {
if (item?.jumpUrl || item?.url) {
message.content.push({
type: ['text', 'qqurl'],
text: item.jumpUrl ?? item.url,
data: {
url: item.jumpUrl ?? item.url,
title: item.title ?? item.desc,
}
} as QQUrlMessage);
break;
}
}
}
break;
default:
console.log('unknown message', chunkData);
}
} catch (_) { }
}

@ -4,6 +4,8 @@ import { ModelRegistry } from "../DatabaseManager";
import { ItemLimitedList } from "../utils/ItemLimitedList";
import { CommonMessage } from "../message/Message";
import { RobotStorage } from "./RobotStorage";
import { Reactive, reactive } from "../utils/reactive";
import { debounce } from "throttle-debounce";
export class MessageStorage {
private app: App;
@ -31,7 +33,7 @@ export class MessageStorage {
public async get(messageId: string): Promise<CommonMessage | null> {
// from cache
let messageObj = this.cache.find((msg) => msg && msg.id === messageId);
let messageObj: CommonMessage | null | undefined = this.cache.find((msg) => msg && msg.id === messageId);
if (messageObj) {
return messageObj;
}
@ -46,7 +48,7 @@ export class MessageStorage {
const robot = this.storages.robot;
if (robot) {
messageObj = await robot.parseDBMessage?.(doc);
return messageObj!;
return messageObj;
} else {
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
}
@ -78,6 +80,29 @@ export class MessageStorage {
this.cache.push(message);
}
/**
* Reactive
* @param message
*/
public reactive<T extends CommonMessage>(message: T): Reactive<T> {
const messageRef = reactive(message);
// debounce
const onDataChanged = debounce(this.cacheTTL * 1000, async () => {
this.app.logger.debug(`Reactive 更新消息: ${message.id}`);
this.set(message).catch((err) => {
this.app.logger.error(`更新消息 ${message.id} 失败:${err.message}`, err);
console.error(err);
});
});
messageRef._on('change', (key: string, val: Reactive<T>) => {
onDataChanged();
});
return messageRef;
}
public async remove(messageId: string): Promise<void> {
if (this.models) {
await this.models.userInfo.deleteOne({

@ -1,7 +1,7 @@
import { Robot } from "#ibot/robot/Robot";
import App from "../App";
import { StorageConfig } from "../Config";
import { ModelRegistry } from "../DatabaseManager";
import { Robot } from "../RobotManager";
import { ChannelInfoStorage } from "./ChannelInfoStorage";
import { GroupInfoStorage } from "./GroupInfoStorage";
import { GroupUserInfoStorage } from "./GroupUserInfoStorage";

@ -0,0 +1,65 @@
import EventEmitter from "events";
export type Reactive<T extends Object = any> = T & {
_on: EventEmitter['on'];
_once: EventEmitter['once'];
_off: EventEmitter['off'];
_isReactive: true;
_value: T;
updated: () => void;
};
export function reactive<T extends Object>(obj: T): Reactive<T> {
const eventEmitter = new EventEmitter();
// 递归监听子对象
for (let key of Object.getOwnPropertyNames(obj)) {
if (key.startsWith('_')) continue;
const value = obj[key as keyof T];
if (typeof value === 'object' && (value as Reactive<{}>)._isReactive) {
(value as Reactive<{}>)._on('change', (childKey: string, newValue: any) => {
eventEmitter.emit('change', key, value);
eventEmitter.emit(`change:${key}`, value);
});
}
}
return new Proxy(obj, {
set: (target, key, value) => {
if (!key.toString().startsWith('_') && typeof value === 'object') {
// 递归监听子对象
if (!value._isReactive) {
value = reactive(value);
}
value._on('change', (childKey: string, newValue: any) => {
eventEmitter.emit('change', key, value);
eventEmitter.emit(`change:${key.toString()}`, value);
});
}
target[key as keyof T] = value;
return true;
},
get: (target, key): any => {
switch (key) {
case '_on':
return eventEmitter.on.bind(eventEmitter);
case '_once':
return eventEmitter.once.bind(eventEmitter);
case '_off':
return eventEmitter.off.bind(eventEmitter);
case '_isReactive':
return true;
case '_value':
return target;
case 'updated':
return () => {
eventEmitter.emit('change', null, null);
};
default:
return target[key as keyof T];
}
}
}) as unknown as Reactive<T>;
}

@ -27,8 +27,10 @@
"module": "ES2022", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
"paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
"paths": {
"#ibot/*": ["./src/*"],
}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */

Loading…
Cancel
Save