更新MessageChunk的结构,增加消息记录器功能
parent
fc78861d7c
commit
8324766ee2
@ -0,0 +1,37 @@
|
||||
import App from "./App";
|
||||
import { StorageConfig } from "./Config";
|
||||
import { RobotStorage } from "./storage/RobotStorage";
|
||||
import { UserInfoStorage } from "./storage/UserInfoStorage";
|
||||
|
||||
export class StorageManager {
|
||||
private app: App;
|
||||
private config: StorageConfig;
|
||||
|
||||
private robotStorages: Record<string, RobotStorage> = {};
|
||||
|
||||
public constructor(app: App, config: StorageConfig) {
|
||||
this.app = app;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
|
||||
}
|
||||
|
||||
public async getStorages(robotId: string): Promise<RobotStorage> {
|
||||
if (!this.app.robot.getRobot(robotId)) {
|
||||
throw new Error(`未找到机器人 ${robotId}`);
|
||||
}
|
||||
|
||||
// 如果已生成则直接返回
|
||||
if (robotId in this.robotStorages) {
|
||||
return this.robotStorages[robotId];
|
||||
}
|
||||
|
||||
const storages = new RobotStorage(this.app, this.config, robotId);
|
||||
|
||||
await storages.initialize();
|
||||
|
||||
return storages;
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import App from "../App";
|
||||
import { buildChatIdentityQuery, toChatIdentityEntity } from "../orm/Message";
|
||||
import { PluginController, PluginEvent } from "../PluginManager";
|
||||
import { TestSchema } from "./test/TestSchema";
|
||||
|
||||
export default class TestController implements PluginController {
|
||||
public event!: PluginEvent;
|
||||
public app: App;
|
||||
|
||||
public id = 'test';
|
||||
public name = '测试功能';
|
||||
public description = '测试功能控制器';
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.event.init(this);
|
||||
|
||||
const dbi = this.app.database;
|
||||
if (!dbi) return;
|
||||
|
||||
const TestModel = dbi.getModel('Test', TestSchema);
|
||||
|
||||
this.event.registerCommand({
|
||||
command: '写入',
|
||||
name: '写入数据库',
|
||||
}, (args, message, resolve) => {
|
||||
resolve();
|
||||
|
||||
return (async () => {
|
||||
let obj = new TestModel({
|
||||
chatIdentity: toChatIdentityEntity(message.sender.identity),
|
||||
data: args,
|
||||
});
|
||||
|
||||
await obj.save();
|
||||
})();
|
||||
});
|
||||
|
||||
this.event.registerCommand({
|
||||
command: '读取',
|
||||
name: '读取数据库',
|
||||
}, async (args, message, resolve) => {
|
||||
resolve();
|
||||
|
||||
let obj = await TestModel.findOne(buildChatIdentityQuery(message.sender.identity));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
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";
|
||||
|
||||
export type WebdavConfig = {
|
||||
url: string,
|
||||
username?: string,
|
||||
password?: string,
|
||||
path?: string,
|
||||
exclusive?: boolean;
|
||||
};
|
||||
|
||||
export default class WebdavFileBackupController implements PluginController {
|
||||
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
|
||||
|
||||
private SESSION_KEY_GENERATE_COUNT = 'stablediffusion_generateCount';
|
||||
|
||||
public event!: PluginEvent;
|
||||
public app: App;
|
||||
public chatGPTClient: any;
|
||||
|
||||
public id = 'webdav_file_backup';
|
||||
public name = 'Webdav文件备份';
|
||||
public description = '将群文件备份到Webdav服务';
|
||||
|
||||
private messageGroup: Record<string, RandomMessage> = {}
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
async getDefaultConfig() {
|
||||
return {
|
||||
groups: {} as Record<string, WebdavConfig>,
|
||||
messages: {
|
||||
error: [
|
||||
'转存群文件失败:{{{error}}}',
|
||||
'在转存群文件时发生了错误:{{{error}}}',
|
||||
'未能将群文件转存到资料库:{{{error}}}',
|
||||
'由于以下错误,文件转存失败:{{{error}}}',
|
||||
'很抱歉,文件无法成功转存至群组资料库,原因是:{{{error}}}。',
|
||||
'转存群组文件时出现问题,错误详情:{{{error}}}。',
|
||||
'文件无法转存到资料库,错误信息如下:{{{error}}}。',
|
||||
'出现错误,导致文件无法成功转存至群组资料库:{{{error}}}。',
|
||||
'转存群文件遇到问题,以下是错误的详细信息:{{{error}}}。',
|
||||
'文件转存失败,原因是:{{{error}}}。',
|
||||
'抱歉,由于以下错误,文件未能成功转存至群组资料库:{{{error}}}。',
|
||||
'在尝试将文件转存至群组资料库时,发生了如下错误:{{{error}}}。',
|
||||
'文件转存操作失败,错误详情:{{{error}}}。',
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async initialize(config: any) {
|
||||
await this.updateConfig(config);
|
||||
|
||||
this.event.init(this);
|
||||
|
||||
this.event.on('message/group', async (message, resolved) => {
|
||||
if (message.type !== 'attachment') return;
|
||||
|
||||
let groupId = message.sender.groupId;
|
||||
if (!groupId) return;
|
||||
|
||||
let groupConfig = this.config.groups[groupId];
|
||||
if (!groupConfig) return;
|
||||
|
||||
if (groupConfig.exclusive) {
|
||||
resolved();
|
||||
}
|
||||
|
||||
return this.uploadGroupFile(message, groupConfig);
|
||||
}, {
|
||||
priority: MessagePriority.HIGH,
|
||||
});
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
|
||||
}
|
||||
|
||||
async updateConfig(config: any) {
|
||||
this.config = config;
|
||||
|
||||
// 随机消息
|
||||
for (let [key, value] of Object.entries(this.config.messages)) {
|
||||
this.messageGroup[key] = new RandomMessage(value);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadGroupFile(message: CommonReceivedMessage, groupConfig: WebdavConfig) {
|
||||
if (message.content[0] &&
|
||||
(message.content[0].type.includes('attachment'))) {
|
||||
let attachmentMsg = message.content[0] as AttachmentMessage;
|
||||
let fileName = attachmentMsg.data.fileName;
|
||||
let url = attachmentMsg.data.url;
|
||||
let fileSize = attachmentMsg.data.size;
|
||||
|
||||
message.markRead()
|
||||
|
||||
this.app.logger.info(`[群号:${message.sender.groupId}] 收到群文件:${fileName},开始转存`);
|
||||
|
||||
let authOption: any = {};
|
||||
if (groupConfig.username) {
|
||||
authOption.username = groupConfig.username;
|
||||
}
|
||||
if (groupConfig.password) {
|
||||
authOption.password = groupConfig.password;
|
||||
}
|
||||
|
||||
let client = createClient(groupConfig.url, groupConfig);
|
||||
|
||||
let filePath = '';
|
||||
if (groupConfig.path) {
|
||||
filePath = groupConfig.path.replace(/\$\{(\w+)\}/g, (match, p1) => {
|
||||
switch (p1) {
|
||||
case 'groupId':
|
||||
return message.sender.groupId;
|
||||
case 'groupName':
|
||||
return message.sender.groupName;
|
||||
case 'userId':
|
||||
case 'uid':
|
||||
return message.sender.userId;
|
||||
case 'fileName':
|
||||
return fileName;
|
||||
case 'date':
|
||||
return message.time.toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
||||
case 'year':
|
||||
return message.time.getFullYear().toString();
|
||||
case 'month':
|
||||
return (message.time.getMonth() + 1).toString();
|
||||
case 'day':
|
||||
return message.time.getDate().toString();
|
||||
case 'timestamp':
|
||||
return message.time.getTime().toString();
|
||||
default:
|
||||
return match;
|
||||
}
|
||||
})
|
||||
} else {
|
||||
filePath = '/' + fileName;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
let fileShortName = fileName.substring(0, 10);
|
||||
if (fileShortName.length !== fileName.length) {
|
||||
fileShortName += '...';
|
||||
}
|
||||
|
||||
// create path
|
||||
let path = filePath.split('/');
|
||||
path.pop();
|
||||
let currentPath = '';
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
currentPath += '/' + path[i];
|
||||
try {
|
||||
if (!await client.exists(currentPath)) {
|
||||
await client.createDirectory(currentPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (await client.exists(filePath)) {
|
||||
let fileExt = extname(filePath);
|
||||
if (fileExt) {
|
||||
filePath = filePath.replace(fileExt, `_${Date.now()}${fileExt}`);
|
||||
} else {
|
||||
filePath = filePath + `_${Date.now()}`;
|
||||
}
|
||||
}
|
||||
|
||||
/*if (fileSize && fileSize > 1024 * 1024 * 10) {
|
||||
await message.sendReply('正在转存文件:' + fileShortName, false);
|
||||
}*/
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
got.stream(url).pipe(client.createWriteStream(filePath))
|
||||
.on('finish', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
// await message.sendReply('文件 ' + fileShortName + ' 已经转存到资料库了', false);
|
||||
} catch(err: any) {
|
||||
this.app.logger.error("转存群文件失败:" + err.message, err);
|
||||
console.error(err);
|
||||
|
||||
let msg = this.messageGroup.error.nextMessage(err.message);
|
||||
await message.sendReply(msg ?? `转存群文件失败:${err.message}`, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import mongoose, { Schema, Types } from "mongoose";
|
||||
import { ChatIdentityEntity, ChatIdentityEntityType } from "../../orm/Message";
|
||||
|
||||
export type TestSchemaType = {
|
||||
id: Types.ObjectId,
|
||||
chatIdentity: ChatIdentityEntityType,
|
||||
data: string,
|
||||
};
|
||||
|
||||
export const TestSchema = new Schema<TestSchemaType>({
|
||||
id: Object,
|
||||
chatIdentity: ChatIdentityEntity,
|
||||
data: String
|
||||
});
|
||||
|
||||
export const TestModel = mongoose.model<TestSchemaType>('Test', TestSchema);
|
@ -0,0 +1,31 @@
|
||||
import { Model, Schema, Types } from "mongoose";
|
||||
import { ChannelInfoType } from "../message/Sender";
|
||||
import { ModelBase } from "../DatabaseManager";
|
||||
|
||||
export type ChannelInfoSchemaType = ChannelInfoType;
|
||||
|
||||
export type ChannelInfoModelType = Model<ChannelInfoSchemaType>;
|
||||
|
||||
export const ChannelInfoSchema = (robotId: string) => new Schema<ChannelInfoSchemaType>({
|
||||
channelId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
index: true,
|
||||
},
|
||||
image: String,
|
||||
extra: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
});
|
||||
|
||||
export const GroupInfoModelBase: ModelBase = {
|
||||
table: 'channel_info',
|
||||
schema: ChannelInfoSchema,
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import mongoose, { Schema, Types } from "mongoose";
|
||||
import { GroupInfoType } from "../message/Sender";
|
||||
|
||||
export type GroupInfoSchemaType = GroupInfoType;
|
||||
|
||||
export type GroupInfoModelType = mongoose.Model<GroupInfoSchemaType>;
|
||||
|
||||
export const GroupInfoSchema = (robotId: string) => new Schema<GroupInfoSchemaType>({
|
||||
groupId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
rootGroupId: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
index: true,
|
||||
},
|
||||
image: String,
|
||||
extra: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
});
|
@ -0,0 +1,35 @@
|
||||
import mongoose, { Schema, Types } from "mongoose";
|
||||
import { GroupUserInfoType } from "../message/Sender";
|
||||
|
||||
export type GroupUserInfoSchemaType = GroupUserInfoType;
|
||||
|
||||
export type GroupUserInfoModelType = mongoose.Model<GroupUserInfoSchemaType>;
|
||||
|
||||
export const GroupUserInfoSchema = (robotId: string) => new Schema<GroupUserInfoSchemaType>({
|
||||
rootGroupId: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
groupId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
userName: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
nickName: String,
|
||||
title: String,
|
||||
role: String,
|
||||
image: String,
|
||||
extra: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
});
|
@ -0,0 +1,129 @@
|
||||
import { Model, Schema, Types } from "mongoose";
|
||||
import { ChatIdentity } from "../message/Sender";
|
||||
import { MessageChunk, MessageDirection } from "../message/Message";
|
||||
|
||||
export function chatIdentityToDB(chatIdentity: ChatIdentity): ChatIdentityEntityType {
|
||||
return {
|
||||
rootGroupId: chatIdentity.rootGroupId,
|
||||
groupId: chatIdentity.groupId,
|
||||
userId: chatIdentity.userId,
|
||||
channelId: chatIdentity.channelId,
|
||||
}
|
||||
}
|
||||
|
||||
export type ChatIdentityEntityType = Partial<{
|
||||
rootGroupId: string,
|
||||
groupId: string,
|
||||
userId: string,
|
||||
channelId: string,
|
||||
}>;
|
||||
|
||||
export type MessageDataType = {
|
||||
/** 消息ID */
|
||||
messageId: string,
|
||||
/** 消息类型 */
|
||||
type: string,
|
||||
/** 消息收发(消息方向) */
|
||||
direction: MessageDirection,
|
||||
/** 聊天类型(私聊、群聊) */
|
||||
chatType: string,
|
||||
/** 聊天目标的ID */
|
||||
chatIdentity: ChatIdentityEntityType,
|
||||
/** 回复的消息ID */
|
||||
repliedMessageId?: string,
|
||||
/** 提到的用户ID */
|
||||
mentionedUserIds?: string[],
|
||||
/** 纯文本消息内容 */
|
||||
contentText: string,
|
||||
/** 消息内容 */
|
||||
content: MessageChunk[],
|
||||
/** 时间 */
|
||||
time: Date,
|
||||
/** 消息是否被删除 */
|
||||
deleted?: boolean,
|
||||
/** 附加信息 */
|
||||
extra: any,
|
||||
};
|
||||
|
||||
export type MessageSchemaType = MessageDataType;
|
||||
|
||||
export type MessageModelMethods = {
|
||||
|
||||
}
|
||||
|
||||
export type MessageModelType = Model<MessageSchemaType, {}, MessageModelMethods>;
|
||||
|
||||
export const MessageSchema = (robotId: string) => new Schema<MessageSchemaType, MessageModelType>({
|
||||
messageId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
direction: {
|
||||
type: Number,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
chatType: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
chatIdentity: {
|
||||
type: {
|
||||
rootGroupId: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
groupId: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
channelId: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
repliedMessageId: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
mentionedUserIds: {
|
||||
type: [
|
||||
{
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
},
|
||||
contentText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
content: [Object],
|
||||
time: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
index: true,
|
||||
},
|
||||
deleted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
index: true,
|
||||
},
|
||||
extra: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
});
|
@ -0,0 +1,23 @@
|
||||
import mongoose, { Schema, Types } from "mongoose";
|
||||
import { RootGroupInfoType } from "../message/Sender";
|
||||
|
||||
export type RootGroupInfoSchemaType = RootGroupInfoType;
|
||||
|
||||
export type RootGroupInfoModelType = mongoose.Model<RootGroupInfoSchemaType>;
|
||||
|
||||
export const RootGroupInfoSchema = (robotId: string) => new Schema<RootGroupInfoSchemaType>({
|
||||
rootGroupId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
image: String,
|
||||
extra: {
|
||||
type: Object,
|
||||
default: {},
|
||||
}
|
||||
});
|
@ -0,0 +1,27 @@
|
||||
import { Model, Schema, Types } from "mongoose";
|
||||
import { UserInfoType } from "../message/Sender";
|
||||
|
||||
export type UserInfoSchemaType = UserInfoType;
|
||||
|
||||
export type UserInfoModelType = Model<UserInfoSchemaType>;
|
||||
|
||||
export const UserInfoSchema = (robotId: string) => new Schema<UserInfoSchemaType, UserInfoModelType>({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
userName: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
nickName: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
image: String,
|
||||
extra: {
|
||||
type: Object,
|
||||
default: {},
|
||||
}
|
||||
});
|
@ -1,22 +0,0 @@
|
||||
import mongoose, { Schema, Types } from "mongoose";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
export type GroupDataSchemaType = {
|
||||
id: Types.ObjectId,
|
||||
groupId: string,
|
||||
parentId: Types.ObjectId,
|
||||
name: string,
|
||||
image: string,
|
||||
extra: any,
|
||||
};
|
||||
|
||||
export const GroupDataSchema = new Schema<GroupDataSchemaType>({
|
||||
id: ObjectId,
|
||||
groupId: String,
|
||||
parentId: ObjectId,
|
||||
name: String,
|
||||
image: String,
|
||||
extra: Object,
|
||||
});
|
||||
|
||||
export const GroupDataModel = mongoose.model<GroupDataSchemaType>('GroupData', GroupDataSchema);
|
@ -1,28 +0,0 @@
|
||||
import mongoose, { Schema, Types } from "mongoose";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
export type GroupUserDataSchemaType = {
|
||||
id: Types.ObjectId,
|
||||
groupId: string,
|
||||
userId: string,
|
||||
userName: string,
|
||||
nickName: string,
|
||||
title: string,
|
||||
role: string,
|
||||
image: string,
|
||||
extra: any,
|
||||
};
|
||||
|
||||
export const GroupUserDataSchema = new Schema<GroupUserDataSchemaType>({
|
||||
id: ObjectId,
|
||||
groupId: String,
|
||||
userId: String,
|
||||
userName: String,
|
||||
nickName: String,
|
||||
title: String,
|
||||
role: String,
|
||||
image: String,
|
||||
extra: Object,
|
||||
});
|
||||
|
||||
export const GroupUserDataModel = mongoose.model<GroupUserDataSchemaType>('GroupUserData', GroupUserDataSchema);
|
@ -1,106 +0,0 @@
|
||||
import { Schema, Types } from "mongoose";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { ChatIdentity } from "../message/Sender";
|
||||
|
||||
export type ChatIdentityEntityType = Partial<{
|
||||
robotId: string,
|
||||
rootGroupId: string,
|
||||
groupId: string,
|
||||
userId: string,
|
||||
channelId: string,
|
||||
}>;
|
||||
|
||||
export const ChatIdentityEntity = {
|
||||
robotId: String,
|
||||
rootGroupId: String,
|
||||
groupId: String,
|
||||
userId: String,
|
||||
channelId: String,
|
||||
};
|
||||
|
||||
export function toChatIdentityEntity(chatIdentity: ChatIdentity): ChatIdentityEntityType {
|
||||
return {
|
||||
robotId: chatIdentity.robot.robotId,
|
||||
rootGroupId: chatIdentity.rootGroupId,
|
||||
groupId: chatIdentity.groupId,
|
||||
userId: chatIdentity.userId,
|
||||
channelId: chatIdentity.channelId,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildChatIdentityQuery(chatIdentityEntity: ChatIdentityEntityType | ChatIdentity, prefix = 'chatIdentity') {
|
||||
const query: any = {};
|
||||
if ((chatIdentityEntity as any).robotId) {
|
||||
query[`${prefix}.robotId`] = (chatIdentityEntity as any).robotId;
|
||||
} else if ((chatIdentityEntity as any).robot && (chatIdentityEntity as any).robot.robotId) {
|
||||
query[`${prefix}.robotId`] = (chatIdentityEntity as any).robot.robotId;
|
||||
}
|
||||
if (chatIdentityEntity.rootGroupId) {
|
||||
query[`${prefix}.rootGroupId`] = chatIdentityEntity.rootGroupId;
|
||||
}
|
||||
if (chatIdentityEntity.groupId) {
|
||||
query[`${prefix}.groupId`] = chatIdentityEntity.groupId;
|
||||
}
|
||||
if (chatIdentityEntity.userId) {
|
||||
query[`${prefix}.userId`] = chatIdentityEntity.userId;
|
||||
}
|
||||
if (chatIdentityEntity.channelId) {
|
||||
query[`${prefix}.channelId`] = chatIdentityEntity.channelId;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
export type MessageSchemaType = {
|
||||
id: Types.ObjectId,
|
||||
messageId: string,
|
||||
type: string,
|
||||
origin: string,
|
||||
chatIdentity: ChatIdentityEntityType,
|
||||
meta: {
|
||||
repliedId: Types.ObjectId,
|
||||
repliedMessageId: string,
|
||||
mentionedUsers: Types.ObjectId[],
|
||||
mentionedUids: string[],
|
||||
},
|
||||
isSend: boolean,
|
||||
contentText: string,
|
||||
content: any,
|
||||
time: Date,
|
||||
deleted: boolean,
|
||||
extra: any,
|
||||
};
|
||||
|
||||
export const MessageSchema = new Schema<MessageSchemaType>({
|
||||
id: ObjectId,
|
||||
messageId: String,
|
||||
type: String,
|
||||
origin: String,
|
||||
chatIdentity: ChatIdentityEntity,
|
||||
meta: {
|
||||
repliedId: ObjectId,
|
||||
repliedMessageId: String,
|
||||
mentionedUsers: {
|
||||
type: [ObjectId],
|
||||
default: []
|
||||
},
|
||||
mentionedUids: {
|
||||
type: [String],
|
||||
default: []
|
||||
}
|
||||
},
|
||||
isSend: Boolean,
|
||||
contentText: String,
|
||||
content: Object,
|
||||
time: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
deleted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
extra: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
});
|
@ -1,22 +0,0 @@
|
||||
import mongoose, { Schema, Types } from "mongoose";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
export type UserDataSchemaType = {
|
||||
id: Types.ObjectId,
|
||||
userId: string,
|
||||
userName: string,
|
||||
nickName: string,
|
||||
image: string,
|
||||
extra: any,
|
||||
};
|
||||
|
||||
export const UserDataSchema = new Schema<UserDataSchemaType>({
|
||||
id: ObjectId,
|
||||
userId: String,
|
||||
userName: String,
|
||||
nickName: String,
|
||||
image: String,
|
||||
extra: Object,
|
||||
});
|
||||
|
||||
export const UserDataModel = mongoose.model<UserDataSchemaType>('UserData', UserDataSchema);
|
@ -0,0 +1,261 @@
|
||||
import App from "../../App";
|
||||
import { compareProps } from "../../utils/func";
|
||||
import QQRobot, { QQRobotConfig } from "../QQRobot";
|
||||
import { QQGroupSender, QQUserSender } from "./Message";
|
||||
import { GroupInfoType, GroupUserInfoType, UserInfoType } from "../../message/Sender";
|
||||
import { CommonMessage } from "src/message/Message";
|
||||
import { RobotStorage } from "src/storage/RobotStorage";
|
||||
|
||||
export type QQGroupInfo = {
|
||||
groupId: string,
|
||||
groupName?: string,
|
||||
memberCount?: number,
|
||||
memberLimit?: number
|
||||
};
|
||||
|
||||
export class QQInfoProvider {
|
||||
private app: App;
|
||||
private robot: QQRobot;
|
||||
private config: QQRobotConfig;
|
||||
private storages?: RobotStorage;
|
||||
|
||||
private infoLoaderTimer: NodeJS.Timer | null = null;
|
||||
|
||||
public groupList: QQGroupInfo[] = [];
|
||||
public userSenderList: Record<string, QQUserSender> = {};
|
||||
public groupSenderList: Record<string, Record<string, QQGroupSender>> = {};
|
||||
|
||||
constructor(app: App, robot: QQRobot, config: QQRobotConfig) {
|
||||
this.app = app;
|
||||
this.robot = robot;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.storages = await this.app.storage.getStorages(this.robot.robotId);
|
||||
|
||||
this.refreshRobotInfo();
|
||||
|
||||
// 每30分钟刷新一次信息
|
||||
this.infoLoaderTimer = setInterval(() => {
|
||||
this.refreshRobotInfo();
|
||||
}, 30 * 60 * 1000);
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
if (this.infoLoaderTimer) {
|
||||
clearInterval(this.infoLoaderTimer);
|
||||
this.infoLoaderTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshRobotInfo() {
|
||||
// 刷新群信息
|
||||
let remoteGroupList = await this.getGroupList();
|
||||
remoteGroupList.forEach((data) => {
|
||||
if (data.group_id) {
|
||||
let oldGroupIndex = this.groupList.findIndex((info) => info.groupId === data.group_id);
|
||||
|
||||
const groupInfo: QQGroupInfo = {
|
||||
groupId: data.group_id,
|
||||
groupName: data.group_name,
|
||||
memberCount: data.member_count,
|
||||
memberLimit: data.max_member_count
|
||||
}
|
||||
|
||||
if (oldGroupIndex !== -1) {
|
||||
const oldGroupInfo = this.groupList[oldGroupIndex];
|
||||
if (compareProps(oldGroupInfo, groupInfo, ['groupName', 'memberCount', 'memberLimit'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.groupList[oldGroupIndex] = groupInfo;
|
||||
} else {
|
||||
this.groupList.push(groupInfo);
|
||||
}
|
||||
|
||||
this.updateGroupInfo(groupInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public saveMessage(message: CommonMessage) {
|
||||
this.storages?.message.set(message).catch((err: any) => {
|
||||
this.app.logger.error(`将消息保存到数据库出错: ${err.message}`);
|
||||
console.error(err);
|
||||
})
|
||||
}
|
||||
|
||||
async getGroupList(): Promise<any[]> {
|
||||
const res = await this.robot.callRobotApi('get_group_list', {});
|
||||
if (res && res.status === 'ok') {
|
||||
return res.data;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getUsersInfo(userIds: string[]): Promise<(UserInfoType | null)[]> {
|
||||
let userInfoList: (UserInfoType | null)[] = [];
|
||||
|
||||
for (let userId of userIds) {
|
||||
if (userId in this.userSenderList) {
|
||||
let userSender = this.userSenderList[userId];
|
||||
userInfoList.push(this.userSenderToUserInfo(userSender));
|
||||
} else {
|
||||
userInfoList.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
return userInfoList;
|
||||
}
|
||||
|
||||
async getGroupInfo(groupId: string, rootGroupId?: string): Promise<GroupInfoType | null> {
|
||||
let localGroupInfo = this.groupList.find((info) => info.groupId === groupId);
|
||||
|
||||
if (localGroupInfo) {
|
||||
return {
|
||||
groupId,
|
||||
name: localGroupInfo.groupName ?? groupId,
|
||||
image: this.getGroupImage(groupId),
|
||||
extra: {
|
||||
memberCount: localGroupInfo.memberCount,
|
||||
memberLimit: localGroupInfo.memberLimit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async getGroupUsersInfo(userIds: string[], groupId: string, rootGroupId?: string): Promise<(GroupUserInfoType | null)[]> {
|
||||
let groupUserInfoList: (GroupUserInfoType | null)[] = [];
|
||||
|
||||
const localList = this.groupSenderList[groupId];
|
||||
|
||||
if (!localList) {
|
||||
return new Array<null>(userIds.length).fill(null);
|
||||
}
|
||||
|
||||
for (let userId of userIds) {
|
||||
if (userId in localList) {
|
||||
let groupSender = localList[userId];
|
||||
groupUserInfoList.push(this.groupSenderToGroupUserInfo(groupSender));
|
||||
}
|
||||
}
|
||||
|
||||
return groupUserInfoList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户头像
|
||||
* @param userId
|
||||
* @returns
|
||||
*/
|
||||
private getUserImage(userId: string) {
|
||||
return `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群头像
|
||||
* @param groupId
|
||||
* @returns
|
||||
*/
|
||||
private getGroupImage(groupId: string) {
|
||||
return `https://p.qlogo.cn/gh/${groupId}/${groupId}/100`
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新群用户信息
|
||||
* @param groupSender
|
||||
* @returns
|
||||
*/
|
||||
public async updateGroupSender(groupSender: QQGroupSender) {
|
||||
let savedGroupSender = this.groupSenderList[groupSender.groupId]?.[groupSender.userId];
|
||||
if (savedGroupSender) {
|
||||
if (compareProps(savedGroupSender, groupSender, ['globalNickName', 'nickName', 'role', 'level', 'title'])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.groupSenderList[groupSender.groupId]) {
|
||||
this.groupSenderList[groupSender.groupId] = {};
|
||||
}
|
||||
|
||||
this.groupSenderList[groupSender.groupId][groupSender.userId] = groupSender;
|
||||
|
||||
const storages = await this.app.storage.getStorages(this.robot.robotId);
|
||||
|
||||
await storages.userInfo.set(this.userSenderToUserInfo(groupSender.userSender));
|
||||
await storages.groupUserInfo.set(
|
||||
this.groupSenderToGroupUserInfo(groupSender),
|
||||
groupSender.userId,
|
||||
groupSender.groupId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* @param userSender
|
||||
* @returns
|
||||
*/
|
||||
public async updateUserSender(userSender: QQUserSender) {
|
||||
let savedUserSender = this.userSenderList[userSender.userId];
|
||||
if (savedUserSender) {
|
||||
if (compareProps(savedUserSender, userSender, ['nickName'])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.userSenderList[userSender.userId] = userSender;
|
||||
|
||||
const storages = await this.app.storage.getStorages(this.robot.robotId);
|
||||
|
||||
this.app.logger.debug(`更新用户信息: ${userSender.userId}`);
|
||||
|
||||
await storages.userInfo.set(this.userSenderToUserInfo(userSender));
|
||||
}
|
||||
|
||||
public async updateGroupInfo(groupInfo: QQGroupInfo) {
|
||||
const storages = await this.app.storage.getStorages(this.robot.robotId);
|
||||
|
||||
await storages.groupInfo.set(this.groupInfoToStorageGroupInfo(groupInfo));
|
||||
|
||||
this.app.logger.debug(`更新群组信息: ${groupInfo.groupId}`);
|
||||
}
|
||||
|
||||
private groupSenderToGroupUserInfo(groupSender: QQGroupSender): GroupUserInfoType {
|
||||
return {
|
||||
groupId: groupSender.groupId,
|
||||
userId: groupSender.userId,
|
||||
userName: groupSender.userName,
|
||||
nickName: groupSender.nickName || groupSender.globalNickName,
|
||||
title: groupSender.title,
|
||||
role: groupSender.role,
|
||||
image: this.getUserImage(groupSender.userId),
|
||||
extra: {},
|
||||
};
|
||||
}
|
||||
|
||||
private userSenderToUserInfo(userSender: QQUserSender): UserInfoType {
|
||||
return {
|
||||
userId: userSender.userId,
|
||||
userName: userSender.userName,
|
||||
nickName: userSender.nickName,
|
||||
image: this.getUserImage(userSender.userId),
|
||||
extra: {},
|
||||
};
|
||||
}
|
||||
|
||||
private groupInfoToStorageGroupInfo(groupInfo: QQGroupInfo): GroupInfoType {
|
||||
return {
|
||||
groupId: groupInfo.groupId,
|
||||
name: groupInfo.groupName ?? groupInfo.groupId,
|
||||
image: this.getGroupImage(groupInfo.groupId),
|
||||
extra: {
|
||||
memberCount: groupInfo.memberCount,
|
||||
memberLimit: groupInfo.memberLimit,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
import App from "../App";
|
||||
import { StorageConfig } from "../Config";
|
||||
import { CacheStore } from "../CacheManager";
|
||||
import { ChannelInfoType, RootGroupInfoType } from "../message/Sender";
|
||||
import { ModelRegistry } from "../DatabaseManager";
|
||||
import { ChannelInfoSchemaType } from "../odm/ChannelInfo";
|
||||
import { RobotStorage } from "./RobotStorage";
|
||||
|
||||
export class ChannelInfoStorage {
|
||||
private app: App;
|
||||
private config: StorageConfig;
|
||||
private storages: RobotStorage;
|
||||
private models?: ModelRegistry;
|
||||
private cacheTTL: number;
|
||||
|
||||
private cache: CacheStore;
|
||||
|
||||
public constructor(app: App, config: StorageConfig, storages: RobotStorage) {
|
||||
this.app = app;
|
||||
this.config = config;
|
||||
this.cacheTTL = config.cache_ttl ?? 86400;
|
||||
this.storages = storages;
|
||||
|
||||
this.cache = app.cache.getStore(['ObjectCache', storages.robotId, 'channel_info']);
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.models = this.storages.models;
|
||||
}
|
||||
|
||||
public async get(channelId: string, fetchFromBot: boolean = false): Promise<ChannelInfoSchemaType | null> {
|
||||
// from cache
|
||||
let channelInfo = await this.cache.get<ChannelInfoSchemaType>(channelId);
|
||||
if (channelInfo) {
|
||||
return channelInfo;
|
||||
}
|
||||
|
||||
if (fetchFromBot) {
|
||||
return await this.fetchFromRobot(channelId);
|
||||
} else if (this.models) {
|
||||
let doc = await this.models.channelInfo.findOne({
|
||||
channelId,
|
||||
});
|
||||
|
||||
if (doc) {
|
||||
channelInfo = doc.toObject();
|
||||
|
||||
await this.cache.set(channelId, channelInfo, this.cacheTTL);
|
||||
return channelInfo;
|
||||
}
|
||||
} else {
|
||||
this.app.logger.warn('未配置 Database');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getByRef(channelInfo: ChannelInfoSchemaType | string): Promise<ChannelInfoSchemaType | null> {
|
||||
if (typeof channelInfo === 'string') {
|
||||
return await this.get(channelInfo, false);
|
||||
} else {
|
||||
return await this.get(channelInfo.channelId, false);
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchFromRobot(channelId: string): Promise<ChannelInfoSchemaType | null> {
|
||||
const robot = this.storages.robot;
|
||||
if (robot) {
|
||||
const channelInfo = await robot.getChannelInfo?.(channelId);
|
||||
if (channelInfo) {
|
||||
return await this.set(channelInfo);
|
||||
}
|
||||
} else {
|
||||
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async set(channelInfo: ChannelInfoType): Promise<ChannelInfoSchemaType> {
|
||||
let data: ChannelInfoSchemaType = {
|
||||
...channelInfo
|
||||
};
|
||||
|
||||
if (this.models) {
|
||||
await this.models.channelInfo.updateOne({
|
||||
channelId: data.channelId,
|
||||
}, data, {
|
||||
upsert: true,
|
||||
setDefaultsOnInsert: true,
|
||||
});
|
||||
}
|
||||
|
||||
await this.cache.set(data.channelId, data, this.cacheTTL);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public async remove(channelId: string): Promise<void> {
|
||||
if (this.models) {
|
||||
await this.models.channelInfo.deleteOne({
|
||||
channelId,
|
||||
});
|
||||
}
|
||||
|
||||
await this.cache.del(channelId);
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
import App from "../App";
|
||||
import { StorageConfig } from "../Config";
|
||||
import { CacheStore } from "../CacheManager";
|
||||
import { GroupInfoType } from "../message/Sender";
|
||||
import { ModelRegistry } from "../DatabaseManager";
|
||||
import { GroupInfoSchemaType } from "../odm/GroupInfo";
|
||||
import { RootGroupInfoSchemaType } from "../odm/RootGroupInfo";
|
||||
import { Types } from "mongoose";
|
||||
import { RobotStorage } from "./RobotStorage";
|
||||
|
||||
export class GroupInfoStorage {
|
||||
private app: App;
|
||||
private config: StorageConfig;
|
||||
private storages: RobotStorage;
|
||||
private models?: ModelRegistry;
|
||||
private cacheTTL: number;
|
||||
|
||||
private cache: CacheStore;
|
||||
|
||||
public constructor(app: App, config: StorageConfig, storages: RobotStorage) {
|
||||
this.app = app;
|
||||
this.config = config;
|
||||
this.cacheTTL = config.cache_ttl ?? 86400;
|
||||
this.storages = storages;
|
||||
|
||||
this.cache = app.cache.getStore(['ObjectCache', storages.robotId, 'group_info']);
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.models = this.storages.models;
|
||||
}
|
||||
|
||||
private makeKey(groupId: string, rootGroupId?: string): string {
|
||||
if (rootGroupId) {
|
||||
return this.cache.makeKey([groupId, rootGroupId]);
|
||||
} else {
|
||||
return groupId;
|
||||
}
|
||||
}
|
||||
|
||||
public async get(groupId: string, rootGroupId?: string, fetchFromBot: boolean = false): Promise<GroupInfoSchemaType | null> {
|
||||
// from cache
|
||||
let groupInfo = await this.cache.get<GroupInfoSchemaType>(this.makeKey(groupId, rootGroupId));
|
||||
if (groupInfo) {
|
||||
return groupInfo;
|
||||
}
|
||||
|
||||
if (fetchFromBot) {
|
||||
return await this.fetchFromRobot(groupId, rootGroupId);
|
||||
} else if (this.models) {
|
||||
let doc = await this.models.groupInfo.findOne(rootGroupId ? {
|
||||
groupId,
|
||||
rootGroupId,
|
||||
} : { groupId });
|
||||
|
||||
if (doc) {
|
||||
groupInfo = doc.toObject();
|
||||
|
||||
await this.cache.set(this.makeKey(groupId, rootGroupId), groupInfo, this.cacheTTL);
|
||||
return groupInfo;
|
||||
}
|
||||
} else {
|
||||
this.app.logger.warn('未配置 Database');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getByRef(groupInfo: GroupInfoSchemaType | string, rootGroupId?: string): Promise<GroupInfoSchemaType | null> {
|
||||
if (typeof groupInfo === 'string') {
|
||||
return await this.get(groupInfo, rootGroupId, false);
|
||||
} else {
|
||||
return await this.get(groupInfo.groupId, groupInfo.rootGroupId, false);
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchFromRobot(groupId: string, rootGroupId?: string): Promise<GroupInfoSchemaType | null> {
|
||||
const robot = this.storages.robot;
|
||||
if (robot) {
|
||||
const groupInfo = await robot.getGroupInfo?.(groupId, rootGroupId);
|
||||
if (groupInfo) {
|
||||
return await this.set(groupInfo, rootGroupId);
|
||||
}
|
||||
} else {
|
||||
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async set(groupInfo: GroupInfoType, rootGroupInfo?: string | RootGroupInfoSchemaType): Promise<GroupInfoSchemaType> {
|
||||
let data: GroupInfoSchemaType = {
|
||||
...groupInfo,
|
||||
rootGroupId: typeof rootGroupInfo === 'string' ? rootGroupInfo : rootGroupInfo?.rootGroupId,
|
||||
};
|
||||
|
||||
if (this.models) {
|
||||
await this.models.groupInfo.updateOne({
|
||||
groupId: data.groupId,
|
||||
rootGroupId: data.rootGroupId,
|
||||
}, data, {
|
||||
upsert: true,
|
||||
setDefaultsOnInsert: true,
|
||||
});
|
||||
}
|
||||
|
||||
await this.cache.set(this.makeKey(data.groupId, data.rootGroupId), data, this.cacheTTL);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public async remove(groupId: string, rootGroupId?: string): Promise<void> {
|
||||
if (this.models) {
|
||||
await this.models.groupInfo.deleteOne({
|
||||
groupId,
|
||||
rootGroupId,
|
||||
});
|
||||
}
|
||||
|
||||
await this.cache.del(this.makeKey(groupId, rootGroupId));
|
||||
}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
import App from "../App";
|
||||
import { StorageConfig } from "../Config";
|
||||
import { CacheStore } from "../CacheManager";
|
||||
import { GroupUserInfoType } from "../message/Sender";
|
||||
import { ModelRegistry } from "../DatabaseManager";
|
||||
import { UserInfoSchemaType } from "../odm/UserInfo";
|
||||
import { GroupInfoSchemaType } from "../odm/GroupInfo";
|
||||
import { RobotStorage } from "./RobotStorage";
|
||||
import { GroupUserInfoSchemaType } from "../odm/GroupUserInfo";
|
||||
import { RootGroupInfoSchemaType } from "../odm/RootGroupInfo";
|
||||
|
||||
export class GroupUserInfoStorage {
|
||||
private app: App;
|
||||
private config: StorageConfig;
|
||||
private storages: RobotStorage;
|
||||
private models?: ModelRegistry;
|
||||
private cacheTTL: number;
|
||||
|
||||
private cache: CacheStore;
|
||||
|
||||
public constructor(app: App, config: StorageConfig, storages: RobotStorage) {
|
||||
this.app = app;
|
||||
this.config = config;
|
||||
this.cacheTTL = config.cache_ttl ?? 86400;
|
||||
this.storages = storages;
|
||||
|
||||
this.cache = app.cache.getStore(['ObjectCache', storages.robotId, 'group_user_info']);
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.models = this.storages.models;
|
||||
}
|
||||
|
||||
private makeKey(userId: string, groupId: string, rootGroupId?: string): string {
|
||||
if (rootGroupId) {
|
||||
return this.cache.makeKey([userId, rootGroupId, groupId]);
|
||||
} else {
|
||||
return this.cache.makeKey([userId, groupId]);
|
||||
}
|
||||
}
|
||||
|
||||
public async get(userId: string, groupId: string, rootGroupId?: string, fetchFromBot: boolean = false): Promise<GroupUserInfoSchemaType | null> {
|
||||
// from cache
|
||||
let groupUserInfo = await this.cache.get<GroupUserInfoSchemaType>(this.makeKey(userId, groupId, rootGroupId));
|
||||
if (groupUserInfo) {
|
||||
return groupUserInfo;
|
||||
}
|
||||
|
||||
if (fetchFromBot) {
|
||||
// from bot
|
||||
return await this.fetchFromRobot(userId, groupId);
|
||||
} else if (this.models) { // from database
|
||||
let doc = await this.models.groupUserInfo.findOne({
|
||||
userId,
|
||||
groupId,
|
||||
rootGroupId,
|
||||
});
|
||||
|
||||
if (doc) {
|
||||
groupUserInfo = doc.toObject();
|
||||
|
||||
await this.cache.set(this.makeKey(userId, groupId, rootGroupId), groupUserInfo, this.cacheTTL);
|
||||
return groupUserInfo;
|
||||
}
|
||||
} else {
|
||||
this.app.logger.warn('未配置 Database');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async fetchFromRobot(userId: string, groupId: string, rootGroupId?: string): Promise<GroupUserInfoSchemaType | null> {
|
||||
const robot = this.app.robot.getRobot(this.storages.robotId);
|
||||
if (robot) {
|
||||
const groupUserInfoList = await robot.getGroupUsersInfo?.([userId], groupId);
|
||||
if (groupUserInfoList && groupUserInfoList.length > 0) {
|
||||
const groupUserInfo = groupUserInfoList[0];
|
||||
if (groupUserInfo) {
|
||||
return await this.set(groupUserInfo, userId, groupId, rootGroupId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async set(groupUserInfo: GroupUserInfoType, userInfo: string | UserInfoSchemaType,
|
||||
groupInfo: string | GroupInfoSchemaType, rootGroupInfo?: string | RootGroupInfoSchemaType): Promise<GroupUserInfoSchemaType> {
|
||||
let data: GroupUserInfoSchemaType = {
|
||||
...groupUserInfo,
|
||||
userId: typeof userInfo === 'string' ? userInfo : userInfo.userId,
|
||||
groupId: typeof groupInfo === 'string' ? groupInfo : groupInfo.groupId,
|
||||
rootGroupId: typeof rootGroupInfo === 'string' ? rootGroupInfo : rootGroupInfo?.rootGroupId,
|
||||
};
|
||||
|
||||
// 保存到数据库
|
||||
if (this.models) {
|
||||
await this.models.groupUserInfo.updateOne({
|
||||
userId: data.userId,
|
||||
groupId: data.groupId,
|
||||
}, data, {
|
||||
upsert: true,
|
||||
setDefaultsOnInsert: true,
|
||||
});
|
||||
}
|
||||
|
||||
await this.cache.set(this.makeKey(data.userId, data.groupId, data.rootGroupId), data, this.cacheTTL);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public async remove(userId: string, groupId: string): Promise<void> {
|
||||
if (this.models) {
|
||||
await this.models.groupUserInfo.deleteOne({
|
||||
userId,
|
||||
groupId,
|
||||
});
|
||||
}
|
||||
|
||||
await this.cache.del(this.makeKey(userId, groupId));
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
import App from "../App";
|
||||
import { StorageConfig } from "../Config";
|
||||
import { ModelRegistry } from "../DatabaseManager";
|
||||
import { ItemLimitedList } from "../utils/ItemLimitedList";
|
||||
import { CommonMessage } from "../message/Message";
|
||||
import { RobotStorage } from "./RobotStorage";
|
||||
|
||||
export class MessageStorage {
|
||||
private app: App;
|
||||
private config: StorageConfig;
|
||||
private storages: RobotStorage;
|
||||
private models?: ModelRegistry;
|
||||
private cacheTTL: number;
|
||||
|
||||
private cache: ItemLimitedList<CommonMessage | undefined>;
|
||||
|
||||
public constructor(app: App, config: StorageConfig, storages: RobotStorage) {
|
||||
this.app = app;
|
||||
this.config = config;
|
||||
this.cacheTTL = config.cache_ttl ?? 86400;
|
||||
this.storages = storages;
|
||||
|
||||
let itemLimit = config.message?.lru_limit ?? 1000;
|
||||
|
||||
this.cache = new ItemLimitedList<CommonMessage | undefined>(itemLimit);
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.models = this.storages.models;
|
||||
}
|
||||
|
||||
public async get(messageId: string): Promise<CommonMessage | null> {
|
||||
// from cache
|
||||
let messageObj = this.cache.find((msg) => msg && msg.id === messageId);
|
||||
if (messageObj) {
|
||||
return messageObj;
|
||||
}
|
||||
|
||||
// from database
|
||||
if (this.models) {
|
||||
let doc = await this.models.message.findOne({
|
||||
messageId
|
||||
});
|
||||
|
||||
if (doc) {
|
||||
const robot = this.storages.robot;
|
||||
if (robot) {
|
||||
messageObj = await robot.parseDBMessage?.(doc);
|
||||
return messageObj!;
|
||||
} else {
|
||||
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.app.logger.warn('未配置 Database');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加或更新消息
|
||||
* @param messageId
|
||||
* @param message
|
||||
*/
|
||||
public async set(message: CommonMessage): Promise<void> {
|
||||
let messageData = message.toDBObject();
|
||||
|
||||
if (this.models) {
|
||||
await this.models.message.updateOne({
|
||||
messageId: message.id!,
|
||||
}, messageData, {
|
||||
upsert: true,
|
||||
setDefaultsOnInsert: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.cache.push(message);
|
||||
}
|
||||
|
||||
public async remove(messageId: string): Promise<void> {
|
||||
if (this.models) {
|
||||
await this.models.userInfo.deleteOne({
|
||||
messageId,
|
||||
});
|
||||
}
|
||||
|
||||
let listIndex = this.cache.findIndex((msg) => msg && msg.id === messageId);
|
||||
this.cache[listIndex] = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记消息为已撤回
|
||||
* @param messageId
|
||||
*/
|
||||
public async markDeleted(messageId: string): Promise<void> {
|
||||
if (this.models) {
|
||||
await this.models.message.updateOne({
|
||||
messageId,
|
||||
}, {
|
||||
deleted: true,
|
||||
});
|
||||
}
|
||||
|
||||
let messageObj = this.cache.find((msg) => msg && msg.id === messageId);
|
||||
if (messageObj) {
|
||||
messageObj.deleted = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
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";
|
||||
import { MessageStorage } from "./MessageStorage";
|
||||
import { RootGroupInfoStorage } from "./RootGroupInfoStorage";
|
||||
import { UserInfoStorage } from "./UserInfoStorage";
|
||||
|
||||
export class RobotStorage {
|
||||
private app: App;
|
||||
private config: StorageConfig;
|
||||
private _robotId: string;
|
||||
private _robot?: Robot;
|
||||
private _models?: ModelRegistry;
|
||||
|
||||
public userInfo: UserInfoStorage;
|
||||
public channelInfo: ChannelInfoStorage;
|
||||
public rootGroupInfo: RootGroupInfoStorage;
|
||||
public groupInfo: GroupInfoStorage;
|
||||
public groupUserInfo: GroupUserInfoStorage;
|
||||
public message: MessageStorage;
|
||||
|
||||
public constructor(app: App, config: StorageConfig, robotId: string) {
|
||||
this.app = app;
|
||||
this.config = config;
|
||||
this._robotId = robotId;
|
||||
|
||||
this.userInfo = new UserInfoStorage(app, config, this);
|
||||
this.channelInfo = new ChannelInfoStorage(app, config, this);
|
||||
this.rootGroupInfo = new RootGroupInfoStorage(app, config, this);
|
||||
this.groupInfo = new GroupInfoStorage(app, config, this);
|
||||
this.groupUserInfo = new GroupUserInfoStorage(app, config, this);
|
||||
this.message = new MessageStorage(app, config, this);
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this._models = await this.app.database?.getModels(this.robotId);
|
||||
this._robot = await this.app.robot.getRobot(this.robotId) ?? undefined;
|
||||
|
||||
await this.userInfo.initialize();
|
||||
await this.channelInfo.initialize();
|
||||
await this.rootGroupInfo.initialize();
|
||||
await this.groupInfo.initialize();
|
||||
await this.groupUserInfo.initialize();
|
||||
await this.message.initialize();
|
||||
}
|
||||
|
||||
public get robotId() {
|
||||
return this._robotId;
|
||||
}
|
||||
|
||||
public get models() {
|
||||
return this._models;
|
||||
}
|
||||
|
||||
public get robot() {
|
||||
return this._robot;
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
import App from "../App";
|
||||
import { StorageConfig } from "../Config";
|
||||
import { CacheStore } from "../CacheManager";
|
||||
import { RootGroupInfoType } from "../message/Sender";
|
||||
import { ModelRegistry } from "../DatabaseManager";
|
||||
import { RobotStorage } from "./RobotStorage";
|
||||
import { RootGroupInfoSchemaType } from "../odm/RootGroupInfo";
|
||||
|
||||
export class RootGroupInfoStorage {
|
||||
private app: App;
|
||||
private config: StorageConfig;
|
||||
private storages: RobotStorage;
|
||||
private models?: ModelRegistry;
|
||||
private cacheTTL: number;
|
||||
|
||||
private cache: CacheStore;
|
||||
|
||||
public constructor(app: App, config: StorageConfig, storages: RobotStorage) {
|
||||
this.app = app;
|
||||
this.config = config;
|
||||
this.cacheTTL = config.cache_ttl ?? 86400;
|
||||
this.storages = storages;
|
||||
|
||||
this.cache = app.cache.getStore(['ObjectCache', storages.robotId, 'root_group_info']);
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.models = this.storages.models;
|
||||
}
|
||||
|
||||
public async get(rootGroupId: string, fetchFromBot: boolean = false): Promise<RootGroupInfoSchemaType | null> {
|
||||
// from cache
|
||||
let rootGroupInfo = await this.cache.get<RootGroupInfoSchemaType>(rootGroupId);
|
||||
if (rootGroupInfo) {
|
||||
return rootGroupInfo;
|
||||
}
|
||||
|
||||
if (fetchFromBot) {
|
||||
return await this.fetchFromRobot(rootGroupId);
|
||||
} else if (this.models) {
|
||||
let doc = await this.models.rootGroupInfo.findOne({
|
||||
rootGroupId,
|
||||
});
|
||||
|
||||
if (doc) {
|
||||
rootGroupInfo = doc.toObject();
|
||||
|
||||
await this.cache.set(rootGroupId, rootGroupInfo, this.cacheTTL);
|
||||
return rootGroupInfo;
|
||||
}
|
||||
} else {
|
||||
this.app.logger.warn('未配置 Database');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getByRef(rootGroupInfo: RootGroupInfoSchemaType | string): Promise<RootGroupInfoSchemaType | null> {
|
||||
if (typeof rootGroupInfo === 'string') {
|
||||
return await this.get(rootGroupInfo, false);
|
||||
} else {
|
||||
return await this.get(rootGroupInfo.rootGroupId, false);
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchFromRobot(rootGroupId: string): Promise<RootGroupInfoSchemaType | null> {
|
||||
const robot = this.app.robot.getRobot(this.storages.robotId);
|
||||
if (robot) {
|
||||
const rootGroupInfo = await robot.getRootGroupInfo?.(rootGroupId);
|
||||
if (rootGroupInfo) {
|
||||
return await this.set(rootGroupInfo);
|
||||
}
|
||||
} else {
|
||||
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async set(rootGroupInfo: RootGroupInfoType): Promise<RootGroupInfoSchemaType> {
|
||||
let data: RootGroupInfoSchemaType = {
|
||||
...rootGroupInfo,
|
||||
};
|
||||
|
||||
if (this.models) {
|
||||
await this.models.rootGroupInfo.updateOne({
|
||||
rootGroupId: data.rootGroupId,
|
||||
}, data, {
|
||||
upsert: true,
|
||||
setDefaultsOnInsert: true,
|
||||
});
|
||||
}
|
||||
|
||||
await this.cache.set(data.rootGroupId, data, this.cacheTTL);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public async remove(rootGroupId: string): Promise<void> {
|
||||
if (this.models) {
|
||||
await this.models.rootGroupInfo.deleteOne({
|
||||
rootGroupId,
|
||||
});
|
||||
}
|
||||
|
||||
await this.cache.del(rootGroupId);
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
import App from "../App";
|
||||
import { StorageConfig } from "../Config";
|
||||
import { CacheStore } from "../CacheManager";
|
||||
import { UserInfoType } from "../message/Sender";
|
||||
import { ModelRegistry } from "../DatabaseManager";
|
||||
import { UserInfoSchemaType } from "../odm/UserInfo";
|
||||
import { RobotStorage } from "./RobotStorage";
|
||||
|
||||
export class UserInfoStorage {
|
||||
private app: App;
|
||||
private config: StorageConfig;
|
||||
private storages: RobotStorage;
|
||||
private models?: ModelRegistry;
|
||||
private cacheTTL: number;
|
||||
|
||||
private cache: CacheStore;
|
||||
|
||||
public constructor(app: App, config: StorageConfig, storages: RobotStorage) {
|
||||
this.app = app;
|
||||
this.config = config;
|
||||
this.cacheTTL = config.cache_ttl ?? 86400;
|
||||
this.storages = storages;
|
||||
|
||||
this.cache = app.cache.getStore(['ObjectCache', storages.robotId, 'user_info']);
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.models = this.storages.models;
|
||||
}
|
||||
|
||||
public async get(userId: string, fetchFromBot: boolean = false): Promise<UserInfoSchemaType | null> {
|
||||
// from cache
|
||||
let userInfo = await this.cache.get<UserInfoSchemaType>(userId);
|
||||
if (userInfo) {
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
if (fetchFromBot) {
|
||||
return await this.fetchFromRobot(userId);
|
||||
} else if (this.models) {
|
||||
let doc = await this.models.userInfo.findOne({
|
||||
userId,
|
||||
});
|
||||
|
||||
if (doc) {
|
||||
userInfo = doc.toObject();
|
||||
|
||||
await this.cache.set(userId, userInfo, this.cacheTTL);
|
||||
return userInfo;
|
||||
}
|
||||
} else {
|
||||
this.app.logger.warn('未配置 Database');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getByRef(userInfo: UserInfoSchemaType | string): Promise<UserInfoSchemaType | null> {
|
||||
if (typeof userInfo === 'string') {
|
||||
return await this.get(userInfo, false);
|
||||
} else {
|
||||
return await this.get(userInfo.userId, false);
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchFromRobot(userId: string): Promise<UserInfoSchemaType | null> {
|
||||
const robot = this.app.robot.getRobot(this.storages.robotId);
|
||||
if (robot) {
|
||||
const userInfoList = await robot.getUsersInfo?.([userId]);
|
||||
if (userInfoList && userInfoList.length > 0) {
|
||||
const userInfo = userInfoList[0];
|
||||
if (userInfo) {
|
||||
return await this.set(userInfo);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async set(userInfo: UserInfoType): Promise<UserInfoSchemaType> {
|
||||
let data: UserInfoSchemaType = {
|
||||
...userInfo,
|
||||
}
|
||||
if (this.models) {
|
||||
await this.models.userInfo.updateOne({
|
||||
userId: data.userId,
|
||||
}, data, {
|
||||
upsert: true,
|
||||
setDefaultsOnInsert: true,
|
||||
});
|
||||
}
|
||||
|
||||
await this.cache.set(data.userId, data, this.cacheTTL);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public async remove(userId: string): Promise<void> {
|
||||
if (this.models) {
|
||||
await this.models.userInfo.deleteOne({
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
await this.cache.del(userId);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
export class ItemLimitedList<T = any> extends Array<T> {
|
||||
private _maxLength: number;
|
||||
|
||||
constructor(maxLength: number) {
|
||||
super();
|
||||
this._maxLength = maxLength;
|
||||
}
|
||||
|
||||
get maxLength() {
|
||||
return this._maxLength;
|
||||
}
|
||||
|
||||
set maxLength(value: number) {
|
||||
this._maxLength = value;
|
||||
if (this.length > this.maxLength) {
|
||||
let offset = this.length - this.maxLength;
|
||||
this.splice(0, offset);
|
||||
}
|
||||
}
|
||||
|
||||
/** 添加元素 */
|
||||
addOne(item: T) {
|
||||
if (this.length + 1 >= this.maxLength) {
|
||||
this.shift();
|
||||
}
|
||||
this.push(item);
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import EventEmitter from "events";
|
||||
import { Utils } from "./Utils";
|
||||
|
||||
export class MessageTypingSimulator extends EventEmitter {
|
||||
public chineseCPM = 1000;
|
||||
public latinCPM = this.chineseCPM * 4;
|
||||
public randomDelay = [0, 3000];
|
||||
|
||||
private messageBuffer: string[] = [];
|
||||
private messageCount = 0;
|
||||
private inTyping = false;
|
||||
private running = true;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public pushMessage(message: string) {
|
||||
this.messageBuffer.push(message);
|
||||
if (!this.inTyping) {
|
||||
this.startTyping();
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.running = false;
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
public async startTyping() {
|
||||
if (this.inTyping) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.inTyping = true;
|
||||
try {
|
||||
while (this.messageBuffer.length > 0 && this.running) {
|
||||
const message = this.messageBuffer.shift();
|
||||
if (!message) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const typingTime = this.getTypingTime(message);
|
||||
// console.log('sleep time', typingTime);
|
||||
await Utils.sleep(typingTime);
|
||||
|
||||
if (this.running) {
|
||||
this.emit('message', message, this.messageCount);
|
||||
this.messageCount++;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.inTyping = false;
|
||||
console.error(e);
|
||||
}
|
||||
this.inTyping = false;
|
||||
}
|
||||
|
||||
private getTypingTime(message: string) {
|
||||
let latinChars = 0;
|
||||
for (let i = 0; i < message.length; i++) {
|
||||
if (message.charCodeAt(i) < 128) {
|
||||
latinChars++;
|
||||
}
|
||||
}
|
||||
let chineseChars = message.length - latinChars;
|
||||
let typingTime = chineseChars * 60000 / this.chineseCPM + latinChars * 60000 / this.latinCPM;
|
||||
typingTime += Math.random() * (this.randomDelay[1] - this.randomDelay[0]) + this.randomDelay[0];
|
||||
|
||||
return typingTime;
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import { ShuffleRandom } from "./ShuffleRandom";
|
||||
import Handlebars from "handlebars";
|
||||
import { Pair } from "./types";
|
||||
|
||||
export class RandomMessage extends ShuffleRandom<Pair<string, HandlebarsTemplateDelegate<any>>> {
|
||||
constructor(messageList: string[] = []) {
|
||||
let itemList: Pair<string, HandlebarsTemplateDelegate<any>>[] = messageList
|
||||
.map((message) => [message, Handlebars.compile(message)]);
|
||||
|
||||
super(itemList);
|
||||
}
|
||||
|
||||
public get messageList(): string[] {
|
||||
return this._itemList.map((item) => item[0]);
|
||||
}
|
||||
|
||||
public set messageList(messageList: string[]) {
|
||||
// Remove message that not in messageList
|
||||
this._itemList = this._itemList.filter((item) => !messageList.includes(item[0]));
|
||||
|
||||
// Add message that not in itemList
|
||||
for (let message of messageList) {
|
||||
if (!this._itemList.some((item) => item[0] === message)) {
|
||||
this._itemList.push([message, Handlebars.compile(message)]);
|
||||
}
|
||||
}
|
||||
|
||||
this.shuffle();
|
||||
}
|
||||
|
||||
public nextMessage(data: any = {}): string | null {
|
||||
let message = super.next();
|
||||
if (message === null) {
|
||||
return null;
|
||||
}
|
||||
let generator = message[1];
|
||||
return generator(data);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
export class ShuffleRandom<T = string> {
|
||||
protected _itemList: T[];
|
||||
protected currentIndex = 0;
|
||||
|
||||
constructor(itemList: T[] = []) {
|
||||
this._itemList = itemList;
|
||||
if (this._itemList.length > 0) {
|
||||
this.shuffle();
|
||||
}
|
||||
}
|
||||
|
||||
public get itemList(): T[] {
|
||||
return this._itemList;
|
||||
}
|
||||
|
||||
public set itemList(itemList: T[]) {
|
||||
this._itemList = itemList;
|
||||
this.shuffle();
|
||||
}
|
||||
|
||||
public next(): T | null {
|
||||
if (this._itemList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let message = this._itemList[this.currentIndex];
|
||||
|
||||
if (this.currentIndex === this._itemList.length - 1) {
|
||||
this.shuffle();
|
||||
} else {
|
||||
this.currentIndex++;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
protected shuffle() {
|
||||
for (let i = 0; i < this._itemList.length; i++) {
|
||||
let j = Math.floor(Math.random() * (i + 1));
|
||||
[this._itemList[i], this._itemList[j]] = [this._itemList[j], this._itemList[i]];
|
||||
}
|
||||
this.currentIndex = 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
export function compareProps(a: any, b: any, props: string[], depth: number = 5): boolean {
|
||||
if (depth <= 0) return true;
|
||||
|
||||
for (let prop of props) {
|
||||
let propPath = prop.split('.');
|
||||
|
||||
if (propPath.length === 1) {
|
||||
// 优化单层性能
|
||||
if (typeof a !== 'object' || typeof b !== 'object' || a[prop] !== b[prop]) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
let curA = a;
|
||||
let curB = b;
|
||||
|
||||
for (let p of propPath) {
|
||||
if (typeof curA !== 'object' || !(p in curA)) {
|
||||
curA = undefined;
|
||||
} else {
|
||||
curA = curA[p];
|
||||
}
|
||||
|
||||
if (typeof curB !== 'object' || !(p in curB)) {
|
||||
curB = undefined;
|
||||
} else {
|
||||
curB = curB[p];
|
||||
}
|
||||
|
||||
if (curA === undefined || curB === undefined) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (curA !== curB) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { MessageChunk } from "src/message/Message";
|
||||
|
||||
export class MessageUtils {
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
export function retrieveOnce<Func extends ((...args: any) => Promise<any>)>(callback: Func): Func {
|
||||
type ValType = Awaited<ReturnType<Func>>;
|
||||
|
||||
let data: any = undefined;
|
||||
let error: Error | undefined = undefined;
|
||||
let loaded = false;
|
||||
let loading = false;
|
||||
|
||||
let callbacks: [(data: ValType) => any, (err: Error) => any][] = [];
|
||||
|
||||
return ((...args: any): Promise<ValType> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (loaded) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
callbacks.push([resolve, reject]);
|
||||
|
||||
if (!loading) {
|
||||
loading = true;
|
||||
callback(...args).then((ret) => {
|
||||
data = ret;
|
||||
loaded = true;
|
||||
loading = false;
|
||||
callbacks.forEach((cb) => {
|
||||
cb[0](ret);
|
||||
});
|
||||
}).catch((err) => {
|
||||
error = err;
|
||||
loaded = true;
|
||||
loading = false;
|
||||
callbacks.forEach((cb) => {
|
||||
cb[1](err);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}) as unknown as Func;
|
||||
}
|
@ -1 +1,5 @@
|
||||
export type AnyFunction = (...args: any) => any;
|
||||
|
||||
export type Pair<T1, T2> = [T1, T2];
|
||||
|
||||
export type LiteralUnion<T extends U, U = string> = T | (U & { zz_IGNORE_ME?: never })
|
Loading…
Reference in New Issue