This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View 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;

View 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;

View 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
View 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
View 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;