优化
This commit is contained in:
1
server/models/README.md
Normal file
1
server/models/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Reference: https://typegoose.github.io/typegoose/docs/guides/quick-start-guide
|
||||
44
server/models/chat/ack.ts
Normal file
44
server/models/chat/ack.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
index,
|
||||
} from '@typegoose/typegoose';
|
||||
import type { Base } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import type { Types } from 'mongoose';
|
||||
import { User } from '../user/user';
|
||||
import { Converse } from './converse';
|
||||
import { Message } from './message';
|
||||
|
||||
/**
|
||||
* 消息已读管理
|
||||
*/
|
||||
@index({ userId: 1, converseId: 1 }, { unique: true }) // 一组userId和converseId应当唯一(用户为先)
|
||||
export class Ack implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop({
|
||||
ref: () => User,
|
||||
})
|
||||
userId: Ref<User>;
|
||||
|
||||
@prop({
|
||||
ref: () => Converse,
|
||||
})
|
||||
converseId: Ref<Converse>;
|
||||
|
||||
@prop({
|
||||
ref: () => Message,
|
||||
})
|
||||
lastMessageId: Ref<Message>;
|
||||
}
|
||||
|
||||
export type AckDocument = DocumentType<Ack>;
|
||||
|
||||
const model = getModelForClass(Ack);
|
||||
|
||||
export type AckModel = typeof model;
|
||||
|
||||
export default model;
|
||||
98
server/models/chat/converse.ts
Normal file
98
server/models/chat/converse.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
ReturnModelType,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import { Types } from 'mongoose';
|
||||
import { NAME_REGEXP } from '../../lib/const';
|
||||
import { User } from '../user/user';
|
||||
|
||||
/**
|
||||
* 设计参考: https://discord.com/developers/docs/resources/channel
|
||||
*/
|
||||
|
||||
const converseType = [
|
||||
'DM', // 私信
|
||||
'Multi', // 多人会话
|
||||
'Group', // 群组
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 聊天会话
|
||||
*/
|
||||
export class Converse extends TimeStamps implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop({
|
||||
trim: true,
|
||||
match: NAME_REGEXP,
|
||||
})
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* 会话类型
|
||||
*/
|
||||
@prop({
|
||||
enum: converseType,
|
||||
type: () => String,
|
||||
})
|
||||
type!: (typeof converseType)[number];
|
||||
|
||||
/**
|
||||
* 会话参与者
|
||||
* DM会话与多人会话有值
|
||||
*/
|
||||
@prop({ ref: () => User })
|
||||
members?: Ref<User>[];
|
||||
|
||||
/**
|
||||
* 查找固定成员已存在的会话
|
||||
*/
|
||||
static async findConverseWithMembers(
|
||||
this: ReturnModelType<typeof Converse>,
|
||||
members: string[]
|
||||
): Promise<DocumentType<Converse> | null> {
|
||||
const converse = await this.findOne({
|
||||
members: {
|
||||
$all: [...members],
|
||||
$size: members.length,
|
||||
},
|
||||
});
|
||||
|
||||
return converse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有加入的会话
|
||||
*/
|
||||
static async findAllJoinedConverseId(
|
||||
this: ReturnModelType<typeof Converse>,
|
||||
userId: string
|
||||
): Promise<string[]> {
|
||||
const conserves = await this.find(
|
||||
{
|
||||
members: new Types.ObjectId(userId),
|
||||
},
|
||||
{
|
||||
_id: 1,
|
||||
}
|
||||
);
|
||||
|
||||
return conserves
|
||||
.map((c) => c.id)
|
||||
.filter(Boolean)
|
||||
.map(String);
|
||||
}
|
||||
}
|
||||
|
||||
export type ConverseDocument = DocumentType<Converse>;
|
||||
|
||||
const model = getModelForClass(Converse);
|
||||
|
||||
export type ConverseModel = typeof model;
|
||||
|
||||
export default model;
|
||||
99
server/models/chat/inbox.ts
Normal file
99
server/models/chat/inbox.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
index,
|
||||
modelOptions,
|
||||
Severity,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import type { Types } from 'mongoose';
|
||||
import type { InboxStruct } from 'tailchat-server-sdk';
|
||||
import { User } from '../user/user';
|
||||
|
||||
/**
|
||||
* @deprecated use InboxStruct directly
|
||||
*/
|
||||
interface InboxMessage {
|
||||
/**
|
||||
* 消息所在群组Id
|
||||
*/
|
||||
groupId?: string;
|
||||
|
||||
/**
|
||||
* 消息所在会话Id
|
||||
*/
|
||||
converseId: string;
|
||||
|
||||
messageId: string;
|
||||
|
||||
/**
|
||||
* 消息发送者
|
||||
*/
|
||||
messageAuthor: string;
|
||||
|
||||
/**
|
||||
* 消息片段,用于消息的预览/发送通知
|
||||
*/
|
||||
messageSnippet: string;
|
||||
|
||||
/**
|
||||
* 消息去除富文本标记的原始内容,用于推送
|
||||
*/
|
||||
messagePlainContent: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收件箱管理
|
||||
*/
|
||||
@modelOptions({
|
||||
options: {
|
||||
allowMixed: Severity.ALLOW,
|
||||
},
|
||||
})
|
||||
@index({ userId: 1 })
|
||||
export class Inbox extends TimeStamps implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
/**
|
||||
* 接收方的id
|
||||
*/
|
||||
@prop({
|
||||
ref: () => User,
|
||||
})
|
||||
userId: Ref<User>;
|
||||
|
||||
@prop({
|
||||
type: () => String,
|
||||
})
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @deprecated please use payload
|
||||
*/
|
||||
@prop()
|
||||
message?: InboxMessage;
|
||||
|
||||
/**
|
||||
* 信息体,没有类型
|
||||
*/
|
||||
@prop()
|
||||
payload?: InboxStruct['payload'];
|
||||
|
||||
/**
|
||||
* 是否已读
|
||||
*/
|
||||
@prop({
|
||||
default: false,
|
||||
})
|
||||
readed: boolean;
|
||||
}
|
||||
|
||||
export type InboxDocument = DocumentType<Inbox>;
|
||||
|
||||
const model = getModelForClass(Inbox);
|
||||
|
||||
export type InboxModel = typeof model;
|
||||
|
||||
export default model;
|
||||
107
server/models/chat/message.ts
Normal file
107
server/models/chat/message.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
ReturnModelType,
|
||||
modelOptions,
|
||||
Severity,
|
||||
index,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Group } from '../group/group';
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import { Converse } from './converse';
|
||||
import { User } from '../user/user';
|
||||
import type { FilterQuery, Types } from 'mongoose';
|
||||
import type { MessageMetaStruct } from 'tailchat-server-sdk';
|
||||
|
||||
class MessageReaction {
|
||||
/**
|
||||
* 消息反应名
|
||||
* 可以直接为emoji表情
|
||||
*/
|
||||
@prop()
|
||||
name: string;
|
||||
|
||||
@prop({ ref: () => User })
|
||||
author?: Ref<User>;
|
||||
}
|
||||
|
||||
@modelOptions({
|
||||
options: {
|
||||
allowMixed: Severity.ALLOW,
|
||||
},
|
||||
})
|
||||
@index({ createdAt: -1 })
|
||||
@index({ converseId: 1, _id: -1 }) // for fetchConverseMessage
|
||||
export class Message extends TimeStamps implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop()
|
||||
content: string;
|
||||
|
||||
@prop({ ref: () => User })
|
||||
author?: Ref<User>;
|
||||
|
||||
@prop({ ref: () => Group })
|
||||
groupId?: Ref<Group>;
|
||||
|
||||
/**
|
||||
* 会话ID 必填
|
||||
* 私信的本质就是创建一个双人的会话
|
||||
*/
|
||||
@prop({ ref: () => Converse, index: true })
|
||||
converseId!: Ref<Converse>;
|
||||
|
||||
@prop({ type: () => MessageReaction })
|
||||
reactions?: MessageReaction[];
|
||||
|
||||
/**
|
||||
* 是否已撤回
|
||||
*/
|
||||
@prop({
|
||||
default: false,
|
||||
})
|
||||
hasRecall: boolean;
|
||||
|
||||
/**
|
||||
* 消息的其他数据
|
||||
*/
|
||||
@prop()
|
||||
meta?: MessageMetaStruct;
|
||||
|
||||
/**
|
||||
* 获取会话消息
|
||||
*/
|
||||
static async fetchConverseMessage(
|
||||
this: ReturnModelType<typeof Message>,
|
||||
converseId: string,
|
||||
startId: string | null,
|
||||
limit = 50
|
||||
) {
|
||||
const conditions: FilterQuery<MessageDocument> = {
|
||||
converseId,
|
||||
};
|
||||
if (startId !== null) {
|
||||
conditions['_id'] = {
|
||||
$lt: startId,
|
||||
};
|
||||
}
|
||||
|
||||
const res = await this.find({ ...conditions })
|
||||
.sort({ _id: -1 })
|
||||
.limit(limit)
|
||||
.exec();
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export type MessageDocument = DocumentType<Message>;
|
||||
|
||||
const model = getModelForClass(Message);
|
||||
|
||||
export type MessageModel = typeof model;
|
||||
|
||||
export default model;
|
||||
79
server/models/config.ts
Normal file
79
server/models/config.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
modelOptions,
|
||||
Severity,
|
||||
ReturnModelType,
|
||||
index,
|
||||
} from '@typegoose/typegoose';
|
||||
import type { Base } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import type { Types } from 'mongoose';
|
||||
|
||||
@modelOptions({
|
||||
options: {
|
||||
allowMixed: Severity.ALLOW,
|
||||
},
|
||||
})
|
||||
@index({ name: 1 })
|
||||
export class Config implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
static globalClientConfigName = '__client_config__';
|
||||
|
||||
@prop()
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* config data
|
||||
*/
|
||||
@prop()
|
||||
data: object;
|
||||
|
||||
static async getAllClientPersistConfig(
|
||||
this: ReturnModelType<typeof Config>
|
||||
): Promise<object> {
|
||||
const config = await this.findOne({
|
||||
name: Config.globalClientConfigName,
|
||||
});
|
||||
|
||||
return config?.data ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* set global client persist config from mongodb
|
||||
*
|
||||
* return all config from db
|
||||
*/
|
||||
static async setClientPersistConfig(
|
||||
this: ReturnModelType<typeof Config>,
|
||||
key: string,
|
||||
value: any
|
||||
): Promise<Record<string, any>> {
|
||||
const newConfig = await this.findOneAndUpdate(
|
||||
{
|
||||
name: Config.globalClientConfigName,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
[`data.${key}`]: value,
|
||||
},
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
new: true,
|
||||
}
|
||||
);
|
||||
|
||||
return newConfig.data;
|
||||
}
|
||||
}
|
||||
|
||||
export type ConfigDocument = DocumentType<Config>;
|
||||
|
||||
const model = getModelForClass(Config);
|
||||
|
||||
export type ConfigModel = typeof model;
|
||||
|
||||
export default model;
|
||||
76
server/models/file.ts
Normal file
76
server/models/file.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
modelOptions,
|
||||
Severity,
|
||||
index,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import type { Types } from 'mongoose';
|
||||
import { User } from './user/user';
|
||||
|
||||
/**
|
||||
* 聊天会话
|
||||
*/
|
||||
@modelOptions({
|
||||
options: {
|
||||
allowMixed: Severity.ALLOW,
|
||||
},
|
||||
})
|
||||
@index({ bucketName: 1, objectName: 1 })
|
||||
@index({ url: 1 })
|
||||
export class File extends TimeStamps implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop()
|
||||
etag: string;
|
||||
|
||||
@prop({ ref: () => User })
|
||||
userId?: Ref<User>;
|
||||
|
||||
@prop()
|
||||
bucketName: string;
|
||||
|
||||
@prop()
|
||||
objectName: string;
|
||||
|
||||
@prop()
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* 文件大小, 单位: Byte
|
||||
*/
|
||||
@prop()
|
||||
size: number;
|
||||
|
||||
/**
|
||||
* 浏览量
|
||||
*/
|
||||
@prop({
|
||||
default: 0,
|
||||
})
|
||||
views: number;
|
||||
|
||||
@prop()
|
||||
metaData: object;
|
||||
|
||||
/**
|
||||
* 这个文件是用于哪里
|
||||
* for example: chat, group, user
|
||||
*/
|
||||
@prop({
|
||||
default: 'unknown',
|
||||
})
|
||||
usage: string;
|
||||
}
|
||||
|
||||
export type FileDocument = DocumentType<File>;
|
||||
|
||||
const model = getModelForClass(File);
|
||||
|
||||
export type FileModel = typeof model;
|
||||
|
||||
export default model;
|
||||
51
server/models/group/group-extra.ts
Normal file
51
server/models/group/group-extra.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
modelOptions,
|
||||
Severity,
|
||||
index,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import type { Types } from 'mongoose';
|
||||
|
||||
/**
|
||||
* Group Extra Data
|
||||
* Use to storage large data when panel load or others
|
||||
*/
|
||||
@modelOptions({
|
||||
options: {
|
||||
allowMixed: Severity.ALLOW,
|
||||
},
|
||||
})
|
||||
@index({ groupId: 1, panelId: 1, name: 1 })
|
||||
export class GroupExtra extends TimeStamps implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop({
|
||||
ref: () => GroupExtra,
|
||||
})
|
||||
groupId: Ref<GroupExtra>;
|
||||
|
||||
@prop()
|
||||
panelId?: string;
|
||||
|
||||
@prop()
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Only allow string
|
||||
*/
|
||||
@prop()
|
||||
data: string;
|
||||
}
|
||||
|
||||
export type GroupExtraDocument = DocumentType<GroupExtra>;
|
||||
|
||||
const model = getModelForClass(GroupExtra);
|
||||
|
||||
export type GroupExtraModel = typeof model;
|
||||
|
||||
export default model;
|
||||
422
server/models/group/group.ts
Normal file
422
server/models/group/group.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
ReturnModelType,
|
||||
modelOptions,
|
||||
Severity,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import _ from 'lodash';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
allPermission,
|
||||
call,
|
||||
GroupPanelType,
|
||||
NoPermissionError,
|
||||
PERMISSION,
|
||||
SYSTEM_USERID,
|
||||
TcContext,
|
||||
} from 'tailchat-server-sdk';
|
||||
import { User } from '../user/user';
|
||||
|
||||
class GroupMember {
|
||||
@prop({
|
||||
type: () => String,
|
||||
})
|
||||
roles?: string[]; // 角色权限组id
|
||||
|
||||
@prop({
|
||||
ref: () => User,
|
||||
})
|
||||
userId: Ref<User>;
|
||||
|
||||
/**
|
||||
* 禁言到xxx 为止
|
||||
*/
|
||||
@prop()
|
||||
muteUntil?: Date;
|
||||
}
|
||||
|
||||
@modelOptions({
|
||||
options: {
|
||||
allowMixed: Severity.ALLOW,
|
||||
},
|
||||
})
|
||||
export class GroupPanel {
|
||||
@prop()
|
||||
id: string; // 在群组中唯一, 可以用任意方式进行生成。这里使用ObjectId, 但不是ObjectId类型
|
||||
|
||||
@prop()
|
||||
name: string; // 用于显示的名称
|
||||
|
||||
@prop()
|
||||
parentId?: string; // 父节点id
|
||||
|
||||
/**
|
||||
* 面板类型:
|
||||
* 0 文本频道
|
||||
* 1 面板分组
|
||||
* 2 插件
|
||||
*
|
||||
* Reference: https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
*/
|
||||
@prop({
|
||||
type: () => Number,
|
||||
})
|
||||
type: GroupPanelType;
|
||||
|
||||
@prop()
|
||||
provider?: string; // 面板提供者,为插件的标识,仅面板类型为插件时有效
|
||||
|
||||
@prop()
|
||||
pluginPanelName?: string; // 插件面板名, 如 com.msgbyte.webview/grouppanel
|
||||
|
||||
/**
|
||||
* 面板的其他数据
|
||||
*/
|
||||
@prop()
|
||||
meta?: object;
|
||||
|
||||
/**
|
||||
* 身份组或者用户的权限
|
||||
* 如果没有设定则应用群组权限
|
||||
*
|
||||
* key 为身份组id或者用户id
|
||||
* value 为权限字符串列表
|
||||
*/
|
||||
@prop()
|
||||
permissionMap?: Record<string, string[]>;
|
||||
|
||||
/**
|
||||
* 所有人的权限列表
|
||||
* 如果没有设定则应用群组权限
|
||||
*/
|
||||
@prop({
|
||||
type: () => String,
|
||||
default: () => [],
|
||||
})
|
||||
fallbackPermissions?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 群组权限组
|
||||
*/
|
||||
export class GroupRole implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop()
|
||||
name: string; // 权限组名
|
||||
|
||||
@prop({
|
||||
type: () => String,
|
||||
})
|
||||
permissions: string[]; // 拥有的权限, 是一段字符串
|
||||
}
|
||||
|
||||
/**
|
||||
* 群组
|
||||
*/
|
||||
@modelOptions({
|
||||
options: {
|
||||
allowMixed: Severity.ALLOW,
|
||||
},
|
||||
})
|
||||
export class Group extends TimeStamps implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop({
|
||||
trim: true,
|
||||
maxlength: [100, 'group name is too long'],
|
||||
})
|
||||
name!: string;
|
||||
|
||||
@prop()
|
||||
avatar?: string;
|
||||
|
||||
@prop({
|
||||
ref: () => User,
|
||||
})
|
||||
owner: Ref<User>;
|
||||
|
||||
@prop({
|
||||
maxlength: 120,
|
||||
})
|
||||
description?: string;
|
||||
|
||||
@prop({ type: () => GroupMember, _id: false })
|
||||
members: GroupMember[];
|
||||
|
||||
@prop({ type: () => GroupPanel, _id: false })
|
||||
panels: GroupPanel[];
|
||||
|
||||
@prop({
|
||||
type: () => GroupRole,
|
||||
default: [],
|
||||
})
|
||||
roles: GroupRole[];
|
||||
|
||||
/**
|
||||
* 所有人的权限列表
|
||||
* 为群组中的最低权限
|
||||
*/
|
||||
@prop({
|
||||
type: () => String,
|
||||
default: () => [],
|
||||
})
|
||||
fallbackPermissions: string[];
|
||||
|
||||
/**
|
||||
* 群组的配置信息
|
||||
*/
|
||||
@prop({ default: () => ({}) })
|
||||
config: object;
|
||||
|
||||
/**
|
||||
* 创建群组
|
||||
*/
|
||||
static async createGroup(
|
||||
this: ReturnModelType<typeof Group>,
|
||||
options: {
|
||||
name: string;
|
||||
avatarBase64?: string; // base64版本的头像字符串
|
||||
panels?: GroupPanel[];
|
||||
owner: string;
|
||||
}
|
||||
): Promise<GroupDocument> {
|
||||
const { name, avatarBase64, panels = [], owner } = options;
|
||||
if (typeof avatarBase64 === 'string') {
|
||||
// TODO: 处理头像上传逻辑
|
||||
}
|
||||
|
||||
// 预处理panels信息, 变换ID为objectid
|
||||
const panelSectionMap: Record<string, string> = {};
|
||||
panels.forEach((panel) => {
|
||||
const originPanelId = panel.id;
|
||||
panel.id = String(new Types.ObjectId());
|
||||
if (panel.type === GroupPanelType.GROUP) {
|
||||
panelSectionMap[originPanelId] = panel.id;
|
||||
}
|
||||
|
||||
if (typeof panel.parentId === 'string') {
|
||||
if (typeof panelSectionMap[panel.parentId] !== 'string') {
|
||||
throw new Error('创建失败, 面板参数不合法');
|
||||
}
|
||||
panel.parentId = panelSectionMap[panel.parentId];
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: Expression produces a union type that is too complex to represent.
|
||||
const res = await this.create({
|
||||
name,
|
||||
panels,
|
||||
owner,
|
||||
members: [
|
||||
{
|
||||
roles: [],
|
||||
userId: owner,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户加入的群组列表
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
static async getUserGroups(
|
||||
this: ReturnModelType<typeof Group>,
|
||||
userId: string
|
||||
): Promise<GroupDocument[]> {
|
||||
return this.find({
|
||||
'members.userId': userId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改群组角色名
|
||||
*/
|
||||
static async updateGroupRoleName(
|
||||
this: ReturnModelType<typeof Group>,
|
||||
groupId: string,
|
||||
roleId: string,
|
||||
roleName: string
|
||||
): Promise<Group> {
|
||||
const group = await this.findById(groupId);
|
||||
if (!group) {
|
||||
throw new Error('Not Found Group');
|
||||
}
|
||||
|
||||
const modifyRole = group.roles.find((role) => String(role._id) === roleId);
|
||||
if (!modifyRole) {
|
||||
throw new Error('Not Found Role');
|
||||
}
|
||||
|
||||
modifyRole.name = roleName;
|
||||
await group.save();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改群组角色权限
|
||||
*/
|
||||
static async updateGroupRolePermission(
|
||||
this: ReturnModelType<typeof Group>,
|
||||
groupId: string,
|
||||
roleId: string,
|
||||
permissions: string[]
|
||||
): Promise<Group> {
|
||||
const group = await this.findById(groupId);
|
||||
if (!group) {
|
||||
throw new Error('Not Found Group');
|
||||
}
|
||||
|
||||
const modifyRole = group.roles.find((role) => String(role._id) === roleId);
|
||||
if (!modifyRole) {
|
||||
throw new Error('Not Found Role');
|
||||
}
|
||||
|
||||
modifyRole.permissions = [...permissions];
|
||||
await group.save();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有权限
|
||||
*/
|
||||
static async getGroupUserPermission(
|
||||
this: ReturnModelType<typeof Group>,
|
||||
groupId: string,
|
||||
userId: string
|
||||
): Promise<string[]> {
|
||||
const group = await this.findById(groupId);
|
||||
if (!group) {
|
||||
throw new Error('Not Found Group');
|
||||
}
|
||||
|
||||
if (userId === SYSTEM_USERID) {
|
||||
return allPermission;
|
||||
}
|
||||
|
||||
const member = group.members.find(
|
||||
(member) => String(member.userId) === userId
|
||||
);
|
||||
|
||||
if (!member) {
|
||||
throw new Error('Not Found Member');
|
||||
}
|
||||
|
||||
const allRoles = member.roles;
|
||||
const allRolesPermission = allRoles.map((roleName) => {
|
||||
const p = group.roles.find((r) => String(r._id) === roleName);
|
||||
|
||||
return p?.permissions ?? [];
|
||||
});
|
||||
|
||||
if (String(group.owner) === userId) {
|
||||
/**
|
||||
* 群组管理者有所有权限
|
||||
* 这里是为了避免插件权限无法预先感知到的问题
|
||||
*/
|
||||
|
||||
return _.uniq([
|
||||
...allPermission,
|
||||
..._.flatten(allRolesPermission),
|
||||
...group.fallbackPermissions,
|
||||
]);
|
||||
} else {
|
||||
return _.uniq([
|
||||
..._.flatten(allRolesPermission),
|
||||
...group.fallbackPermissions,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查群组字段操作权限,如果没有权限会直接抛出异常
|
||||
*/
|
||||
static async checkGroupFieldPermission<
|
||||
K extends keyof Pick<GroupMember, 'roles' | 'muteUntil'>
|
||||
>(
|
||||
this: ReturnModelType<typeof Group>,
|
||||
ctx: TcContext,
|
||||
groupId: string,
|
||||
fieldName: K
|
||||
) {
|
||||
const userId = ctx.meta.userId;
|
||||
const t = ctx.meta.t;
|
||||
|
||||
if (fieldName === 'roles') {
|
||||
// 检查操作用户是否有管理角色的权限
|
||||
const [hasRolePermission] = await call(ctx).checkUserPermissions(
|
||||
groupId,
|
||||
userId,
|
||||
[PERMISSION.core.manageRoles]
|
||||
);
|
||||
if (!hasRolePermission) {
|
||||
throw new NoPermissionError(t('没有操作角色权限'));
|
||||
}
|
||||
} else {
|
||||
// 检查操作用户是否有管理用户权限
|
||||
const [hasUserPermission] = await call(ctx).checkUserPermissions(
|
||||
groupId,
|
||||
userId,
|
||||
[PERMISSION.core.manageUser]
|
||||
);
|
||||
if (!hasUserPermission) {
|
||||
throw new NoPermissionError(t('没有操作用户权限'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改群组成员的字段信息
|
||||
*
|
||||
* 带权限验证
|
||||
*/
|
||||
static async updateGroupMemberField<
|
||||
K extends keyof Pick<GroupMember, 'roles' | 'muteUntil'>
|
||||
>(
|
||||
this: ReturnModelType<typeof Group>,
|
||||
ctx: TcContext,
|
||||
groupId: string,
|
||||
memberId: string,
|
||||
fieldName: K,
|
||||
fieldValue: GroupMember[K] | ((member: GroupMember) => void)
|
||||
): Promise<Group> {
|
||||
const group = await this.findById(groupId);
|
||||
const t = ctx.meta.t;
|
||||
|
||||
await this.checkGroupFieldPermission(ctx, groupId, fieldName);
|
||||
|
||||
const member = group.members.find((m) => String(m.userId) === memberId);
|
||||
if (!member) {
|
||||
throw new Error(t('没有找到该成员'));
|
||||
}
|
||||
|
||||
if (typeof fieldValue === 'function') {
|
||||
fieldValue(member);
|
||||
} else {
|
||||
member[fieldName] = fieldValue;
|
||||
}
|
||||
|
||||
await group.save();
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
export type GroupDocument = DocumentType<Group>;
|
||||
|
||||
const model = getModelForClass(Group);
|
||||
|
||||
export type GroupModel = typeof model;
|
||||
|
||||
export default model;
|
||||
112
server/models/group/invite.ts
Normal file
112
server/models/group/invite.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
ReturnModelType,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import moment from 'moment';
|
||||
import type { Types } from 'mongoose';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { User } from '../user/user';
|
||||
import { Group } from './group';
|
||||
import promiseRetry from 'promise-retry';
|
||||
|
||||
function generateCode() {
|
||||
return nanoid(8);
|
||||
}
|
||||
|
||||
export class GroupInvite extends TimeStamps implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop({
|
||||
index: true,
|
||||
unique: true,
|
||||
default: () => generateCode(),
|
||||
})
|
||||
code!: string;
|
||||
|
||||
@prop({
|
||||
ref: () => User,
|
||||
})
|
||||
creator: Ref<User>;
|
||||
|
||||
@prop({
|
||||
ref: () => Group,
|
||||
})
|
||||
groupId!: Ref<Group>;
|
||||
|
||||
/**
|
||||
* 过期时间,如果不存在则永不过期
|
||||
*/
|
||||
@prop()
|
||||
expiredAt?: Date;
|
||||
|
||||
/**
|
||||
* 被使用次数
|
||||
*/
|
||||
@prop({
|
||||
default: 0,
|
||||
})
|
||||
usage: number;
|
||||
|
||||
/**
|
||||
* 使用上限,如果为空则不限制
|
||||
*/
|
||||
@prop()
|
||||
usageLimit?: number;
|
||||
|
||||
/**
|
||||
* 创建群组邀请
|
||||
* @param groupId 群组id
|
||||
* @param type 普通(7天) 永久
|
||||
*/
|
||||
static async createGroupInvite(
|
||||
this: ReturnModelType<typeof GroupInvite>,
|
||||
groupId: string,
|
||||
creator: string,
|
||||
inviteType: 'normal' | 'permanent'
|
||||
): Promise<GroupInviteDocument> {
|
||||
let expiredAt = moment().add(7, 'day').toDate(); // 默认7天
|
||||
if (inviteType === 'permanent') {
|
||||
expiredAt = undefined;
|
||||
}
|
||||
|
||||
const code = await promiseRetry(
|
||||
async () => {
|
||||
const code = generateCode();
|
||||
const exists = await this.exists({ code });
|
||||
|
||||
if (exists) {
|
||||
throw new Error('Cannot find unused invite code, please try again.');
|
||||
}
|
||||
|
||||
return code;
|
||||
},
|
||||
{
|
||||
minTimeout: 0,
|
||||
maxTimeout: 0,
|
||||
retries: 5,
|
||||
}
|
||||
);
|
||||
|
||||
const invite = await this.create({
|
||||
groupId,
|
||||
code,
|
||||
creator,
|
||||
expiredAt,
|
||||
});
|
||||
|
||||
return invite;
|
||||
}
|
||||
}
|
||||
|
||||
export type GroupInviteDocument = DocumentType<GroupInvite>;
|
||||
|
||||
const model = getModelForClass(GroupInvite);
|
||||
|
||||
export type GroupInviteModel = typeof model;
|
||||
|
||||
export default model;
|
||||
20
server/models/openapi/__tests__/app.spec.ts
Normal file
20
server/models/openapi/__tests__/app.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { filterAvailableAppCapability } from '../app';
|
||||
|
||||
describe('openapp', () => {
|
||||
describe('filterAvailableAppCapability', () => {
|
||||
test.each([
|
||||
[['bot'], ['bot']],
|
||||
[['bot', 'foo'], ['bot']],
|
||||
[
|
||||
['bot', 'webpage', 'oauth'],
|
||||
['bot', 'webpage', 'oauth'],
|
||||
],
|
||||
[
|
||||
['bot', 'webpage', 'oauth', 'a', 'b', 'c'],
|
||||
['bot', 'webpage', 'oauth'],
|
||||
],
|
||||
])('%p', (input, output) => {
|
||||
expect(filterAvailableAppCapability(input)).toEqual(output);
|
||||
});
|
||||
});
|
||||
});
|
||||
105
server/models/openapi/app.ts
Normal file
105
server/models/openapi/app.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
index,
|
||||
ReturnModelType,
|
||||
Ref,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import type { Types } from 'mongoose';
|
||||
import { User } from '../user/user';
|
||||
|
||||
const openAppCapability = [
|
||||
'bot', // 机器人
|
||||
'webpage', // 网页
|
||||
'oauth', // 第三方登录
|
||||
] as const;
|
||||
|
||||
type OpenAppCapability = typeof openAppCapability[number];
|
||||
|
||||
/**
|
||||
* 确保输出类型为应用能力
|
||||
*/
|
||||
export function filterAvailableAppCapability(
|
||||
input: string[]
|
||||
): OpenAppCapability[] {
|
||||
return input.filter((item) =>
|
||||
openAppCapability.includes(item as OpenAppCapability)
|
||||
) as OpenAppCapability[];
|
||||
}
|
||||
|
||||
export interface OpenAppOAuth {
|
||||
redirectUrls: string[];
|
||||
}
|
||||
|
||||
export interface OpenAppBot {
|
||||
callbackUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开放平台应用
|
||||
*/
|
||||
@index({ appId: 1 }, { unique: true })
|
||||
export class OpenApp extends TimeStamps implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop({
|
||||
ref: () => User,
|
||||
})
|
||||
owner: Ref<User>;
|
||||
|
||||
@prop()
|
||||
appId: string;
|
||||
|
||||
@prop()
|
||||
appSecret: string;
|
||||
|
||||
@prop()
|
||||
appName: string;
|
||||
|
||||
@prop()
|
||||
appDesc: string;
|
||||
|
||||
@prop()
|
||||
appIcon: string; // url
|
||||
|
||||
@prop({
|
||||
enum: openAppCapability,
|
||||
type: () => String,
|
||||
})
|
||||
capability: OpenAppCapability[];
|
||||
|
||||
@prop()
|
||||
oauth?: OpenAppOAuth;
|
||||
|
||||
@prop()
|
||||
bot?: OpenAppBot;
|
||||
|
||||
/**
|
||||
* 根据appId获取openapp的实例
|
||||
* 用于获得获得完整数据(包括secret)
|
||||
* 并顺便判断是否拥有该开放平台用户的修改权限
|
||||
*/
|
||||
static async findAppByIdAndOwner(
|
||||
this: ReturnModelType<typeof OpenApp>,
|
||||
appId: string,
|
||||
ownerId: string
|
||||
) {
|
||||
const res = await this.findOne({
|
||||
appId,
|
||||
owner: ownerId,
|
||||
}).exec();
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export type OpenAppDocument = DocumentType<OpenApp>;
|
||||
|
||||
const model = getModelForClass(OpenApp);
|
||||
|
||||
export type OpenAppModel = typeof model;
|
||||
|
||||
export default model;
|
||||
56
server/models/plugin/manifest.ts
Normal file
56
server/models/plugin/manifest.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
ReturnModelType,
|
||||
modelOptions,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import type { Types } from 'mongoose';
|
||||
import { User } from '../user/user';
|
||||
|
||||
export class PluginManifest extends TimeStamps implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop()
|
||||
label: string;
|
||||
|
||||
@prop({
|
||||
unique: true,
|
||||
})
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 插件入口地址
|
||||
*/
|
||||
@prop()
|
||||
url: string;
|
||||
|
||||
@prop()
|
||||
icon?: string;
|
||||
|
||||
@prop()
|
||||
version: string;
|
||||
|
||||
@prop()
|
||||
author: string;
|
||||
|
||||
@prop()
|
||||
description: string;
|
||||
|
||||
@prop()
|
||||
requireRestart: string;
|
||||
|
||||
@prop({ ref: () => User })
|
||||
uploader?: Ref<User>;
|
||||
}
|
||||
|
||||
export type PluginManifestDocument = DocumentType<PluginManifest>;
|
||||
|
||||
const model = getModelForClass(PluginManifest);
|
||||
|
||||
export type PluginManifestModel = typeof model;
|
||||
|
||||
export default model;
|
||||
47
server/models/user/dmlist.ts
Normal file
47
server/models/user/dmlist.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
modelOptions,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Base, FindOrCreate } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import { Converse } from '../chat/converse';
|
||||
import { User } from './user';
|
||||
import findorcreate from 'mongoose-findorcreate';
|
||||
import { plugin } from '@typegoose/typegoose';
|
||||
import type { Types } from 'mongoose';
|
||||
|
||||
/**
|
||||
* 用户私信列表管理
|
||||
*/
|
||||
|
||||
@plugin(findorcreate)
|
||||
@modelOptions({
|
||||
schemaOptions: {
|
||||
collection: 'userdmlist',
|
||||
},
|
||||
})
|
||||
export class UserDMList extends FindOrCreate implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop({
|
||||
ref: () => User,
|
||||
index: true,
|
||||
})
|
||||
userId: Ref<User>;
|
||||
|
||||
@prop({
|
||||
ref: () => Converse,
|
||||
})
|
||||
converseIds: Ref<Converse>[];
|
||||
}
|
||||
|
||||
export type UserDMListDocument = DocumentType<UserDMList>;
|
||||
|
||||
const model = getModelForClass(UserDMList);
|
||||
|
||||
export type UserDMListModel = typeof model;
|
||||
|
||||
export default model;
|
||||
67
server/models/user/friend.ts
Normal file
67
server/models/user/friend.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
plugin,
|
||||
ReturnModelType,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Base, FindOrCreate } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import { User } from './user';
|
||||
import findorcreate from 'mongoose-findorcreate';
|
||||
import type { Types } from 'mongoose';
|
||||
|
||||
/**
|
||||
* 好友请求
|
||||
* 单向好友结构
|
||||
*/
|
||||
@plugin(findorcreate)
|
||||
export class Friend extends FindOrCreate implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop({
|
||||
ref: () => User,
|
||||
index: true,
|
||||
})
|
||||
from: Ref<User>;
|
||||
|
||||
@prop({
|
||||
ref: () => User,
|
||||
})
|
||||
to: Ref<User>;
|
||||
|
||||
/**
|
||||
* 好友昵称, 覆盖用户自己的昵称
|
||||
*/
|
||||
@prop()
|
||||
nickname?: string;
|
||||
|
||||
@prop()
|
||||
createdAt: Date;
|
||||
|
||||
static async buildFriendRelation(
|
||||
this: ReturnModelType<FriendModel>,
|
||||
user1: string,
|
||||
user2: string
|
||||
) {
|
||||
await Promise.all([
|
||||
this.findOrCreate({
|
||||
from: user1,
|
||||
to: user2,
|
||||
}),
|
||||
this.findOrCreate({
|
||||
from: user2,
|
||||
to: user1,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export type FriendDocument = DocumentType<Friend>;
|
||||
|
||||
const model = getModelForClass(Friend);
|
||||
|
||||
export type FriendModel = typeof model;
|
||||
|
||||
export default model;
|
||||
36
server/models/user/friendRequest.ts
Normal file
36
server/models/user/friendRequest.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
} from '@typegoose/typegoose';
|
||||
import type { Base } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import type { Types } from 'mongoose';
|
||||
import { User } from './user';
|
||||
|
||||
/**
|
||||
* 好友请求
|
||||
*/
|
||||
|
||||
export class FriendRequest implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop({
|
||||
ref: () => User,
|
||||
index: true,
|
||||
})
|
||||
from: Ref<User>;
|
||||
|
||||
@prop({
|
||||
ref: () => User,
|
||||
})
|
||||
to: Ref<User>;
|
||||
|
||||
@prop()
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type FriendRequestDocument = DocumentType<FriendRequest>;
|
||||
|
||||
export default getModelForClass(FriendRequest);
|
||||
191
server/models/user/mail.ts
Normal file
191
server/models/user/mail.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
Ref,
|
||||
modelOptions,
|
||||
Severity,
|
||||
ReturnModelType,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import type { Types } from 'mongoose';
|
||||
import { User } from './user';
|
||||
import nodemailer, { Transporter, SendMailOptions } from 'nodemailer';
|
||||
import { parseConnectionUrl } from 'nodemailer/lib/shared';
|
||||
import { config } from 'tailchat-server-sdk';
|
||||
import type SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
|
||||
/**
|
||||
* 将地址格式化
|
||||
*/
|
||||
function stringifyAddress(address: SendMailOptions['to']): string {
|
||||
if (Array.isArray(address)) {
|
||||
return address.map((a) => stringifyAddress(a)).join(',');
|
||||
}
|
||||
|
||||
if (typeof address === 'string') {
|
||||
return address;
|
||||
} else if (address === undefined) {
|
||||
return '';
|
||||
} else if (typeof address === 'object') {
|
||||
return `"${address.name}" ${address.address}`;
|
||||
}
|
||||
}
|
||||
|
||||
function getSMTPConnectionOptions(): SMTPConnection.Options | null {
|
||||
if (config.smtp.connectionUrl) {
|
||||
return parseConnectionUrl(config.smtp.connectionUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@modelOptions({
|
||||
options: {
|
||||
allowMixed: Severity.ALLOW,
|
||||
},
|
||||
})
|
||||
export class Mail extends TimeStamps implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* 发件人邮箱
|
||||
*/
|
||||
@prop()
|
||||
from: string;
|
||||
|
||||
/**
|
||||
* 收件人邮箱
|
||||
*/
|
||||
@prop()
|
||||
to: string;
|
||||
|
||||
/**
|
||||
* 邮件主题
|
||||
*/
|
||||
@prop()
|
||||
subject: string;
|
||||
|
||||
/**
|
||||
* 邮件内容
|
||||
*/
|
||||
@prop()
|
||||
body: string;
|
||||
|
||||
@prop()
|
||||
host?: string;
|
||||
|
||||
@prop()
|
||||
port?: string;
|
||||
|
||||
@prop()
|
||||
secure?: boolean;
|
||||
|
||||
@prop()
|
||||
is_success: boolean;
|
||||
|
||||
@prop()
|
||||
data?: any;
|
||||
|
||||
@prop()
|
||||
error?: string;
|
||||
|
||||
/**
|
||||
* 创建邮件发送实例
|
||||
*/
|
||||
static createMailerTransporter(): Transporter | null {
|
||||
const options = getSMTPConnectionOptions();
|
||||
|
||||
if (options) {
|
||||
const transporter = nodemailer.createTransport(options);
|
||||
|
||||
return transporter;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮件服务是否可用
|
||||
*/
|
||||
static async verifyMailService(): Promise<boolean> {
|
||||
try {
|
||||
const transporter = Mail.createMailerTransporter();
|
||||
|
||||
if (!transporter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const verify = await transporter.verify();
|
||||
return verify;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮件
|
||||
*/
|
||||
static async sendMail(
|
||||
this: ReturnModelType<typeof Mail>,
|
||||
mailOptions: SendMailOptions
|
||||
) {
|
||||
try {
|
||||
const transporter = Mail.createMailerTransporter();
|
||||
if (!transporter) {
|
||||
throw new Error('Mail Transporter is null');
|
||||
}
|
||||
|
||||
const options = {
|
||||
from: config.smtp.senderName,
|
||||
...mailOptions,
|
||||
};
|
||||
|
||||
const smtpOptions = getSMTPConnectionOptions();
|
||||
try {
|
||||
const info = await transporter.sendMail(options);
|
||||
|
||||
await this.create({
|
||||
from: stringifyAddress(options.from),
|
||||
to: stringifyAddress(options.to),
|
||||
subject: options.subject,
|
||||
body: options.html,
|
||||
host: smtpOptions.host,
|
||||
port: smtpOptions.port,
|
||||
secure: smtpOptions.secure,
|
||||
is_success: true,
|
||||
data: info,
|
||||
});
|
||||
|
||||
return info;
|
||||
} catch (err) {
|
||||
this.create({
|
||||
from: stringifyAddress(options.from),
|
||||
to: stringifyAddress(options.to),
|
||||
subject: options.subject,
|
||||
body: options.html,
|
||||
host: smtpOptions.host,
|
||||
port: smtpOptions.port,
|
||||
secure: smtpOptions.secure,
|
||||
is_success: false,
|
||||
error: String(err),
|
||||
});
|
||||
|
||||
throw err;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type MailDocument = DocumentType<Mail>;
|
||||
|
||||
const model = getModelForClass(Mail);
|
||||
|
||||
export type MailModel = typeof model;
|
||||
|
||||
export default model;
|
||||
191
server/models/user/user.ts
Normal file
191
server/models/user/user.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import {
|
||||
getModelForClass,
|
||||
prop,
|
||||
DocumentType,
|
||||
ReturnModelType,
|
||||
modelOptions,
|
||||
Severity,
|
||||
index,
|
||||
} from '@typegoose/typegoose';
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
|
||||
import type { Types } from 'mongoose';
|
||||
import { UserType, userType } from 'tailchat-server-sdk';
|
||||
|
||||
type BaseUserInfo = Pick<User, 'nickname' | 'discriminator' | 'avatar'>;
|
||||
|
||||
/**
|
||||
* 用户设置
|
||||
*/
|
||||
export interface UserSettings {
|
||||
/**
|
||||
* 消息列表虚拟化
|
||||
*/
|
||||
messageListVirtualization?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface UserLoginRes extends User {
|
||||
token: string;
|
||||
}
|
||||
|
||||
@modelOptions({
|
||||
options: {
|
||||
allowMixed: Severity.ALLOW,
|
||||
},
|
||||
})
|
||||
@index({ avatar: 1 })
|
||||
export class User extends TimeStamps implements Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* 用户名 不可被修改
|
||||
* 与email必有一个
|
||||
*/
|
||||
@prop()
|
||||
username?: string;
|
||||
|
||||
/**
|
||||
* 邮箱 不可被修改
|
||||
* 必填
|
||||
*/
|
||||
@prop({
|
||||
index: true,
|
||||
unique: true,
|
||||
})
|
||||
email: string;
|
||||
|
||||
@prop()
|
||||
password!: string;
|
||||
|
||||
/**
|
||||
* 可以被修改的显示名
|
||||
*/
|
||||
@prop({
|
||||
trim: true,
|
||||
maxlength: 20,
|
||||
})
|
||||
nickname!: string;
|
||||
|
||||
/**
|
||||
* 识别器, 跟username构成全局唯一的用户名
|
||||
* 用于搜索
|
||||
* <username>#<discriminator>
|
||||
*/
|
||||
@prop()
|
||||
discriminator: string;
|
||||
|
||||
/**
|
||||
* 是否为临时用户
|
||||
* @default false
|
||||
*/
|
||||
@prop({
|
||||
default: false,
|
||||
})
|
||||
temporary: boolean;
|
||||
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
@prop()
|
||||
avatar?: string;
|
||||
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
@prop({
|
||||
enum: userType,
|
||||
type: () => String,
|
||||
default: 'normalUser',
|
||||
})
|
||||
type: UserType;
|
||||
|
||||
/**
|
||||
* 邮箱是否可用
|
||||
*/
|
||||
@prop({
|
||||
default: false,
|
||||
})
|
||||
emailVerified: boolean;
|
||||
|
||||
/**
|
||||
* 是否被封禁
|
||||
*/
|
||||
@prop({
|
||||
default: false,
|
||||
})
|
||||
banned: boolean;
|
||||
|
||||
/**
|
||||
* 用户的额外信息
|
||||
*/
|
||||
@prop()
|
||||
extra?: object;
|
||||
|
||||
/**
|
||||
* 用户设置
|
||||
*/
|
||||
@prop({
|
||||
default: {},
|
||||
})
|
||||
settings: UserSettings;
|
||||
|
||||
/**
|
||||
* 生成身份识别器
|
||||
* 0001 - 9999
|
||||
*/
|
||||
public static generateDiscriminator(
|
||||
this: ReturnModelType<typeof User>,
|
||||
nickname: string
|
||||
): Promise<string> {
|
||||
let restTimes = 10; // 最多找10次
|
||||
const checkDiscriminator = async () => {
|
||||
const discriminator = String(
|
||||
Math.floor(Math.random() * 9999) + 1
|
||||
).padStart(4, '0');
|
||||
|
||||
const doc = await this.findOne({
|
||||
nickname,
|
||||
discriminator,
|
||||
}).exec();
|
||||
restTimes--;
|
||||
|
||||
if (doc !== null) {
|
||||
// 已存在, 换一个
|
||||
if (restTimes <= 0) {
|
||||
throw new Error('Cannot find space discriminator');
|
||||
}
|
||||
|
||||
return checkDiscriminator();
|
||||
}
|
||||
|
||||
return discriminator;
|
||||
};
|
||||
|
||||
return checkDiscriminator();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户基本信息
|
||||
*/
|
||||
static async getUserBaseInfo(
|
||||
this: ReturnModelType<typeof User>,
|
||||
userId: string
|
||||
): Promise<BaseUserInfo> {
|
||||
const user = await this.findById(String(userId));
|
||||
|
||||
return {
|
||||
nickname: user.nickname,
|
||||
discriminator: user.discriminator,
|
||||
avatar: user.avatar,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type UserDocument = DocumentType<User>;
|
||||
|
||||
const model = getModelForClass(User);
|
||||
|
||||
export type UserModel = typeof model;
|
||||
|
||||
export default model;
|
||||
Reference in New Issue
Block a user