diff --git a/Fabric.Backend.Guild/package-lock.json b/Fabric.Backend.Guild/package-lock.json index 3a13394..b4aea73 100644 --- a/Fabric.Backend.Guild/package-lock.json +++ b/Fabric.Backend.Guild/package-lock.json @@ -12,6 +12,8 @@ "@nestjs/core": "^10.4.8", "@nestjs/platform-express": "^10.4.8", "@nestjs/typeorm": "^11.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", "mysql2": "^3.22.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -592,6 +594,12 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", @@ -1290,6 +1298,23 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", + "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2592,6 +2617,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.1.tgz", + "integrity": "sha512-GEw0GLL7YUUA6nv21IsCvVjtI5Ejn84sjbdfQ9KxdbqEVOk1PZh7xejn01EEiniKw+dBeCfim+8MGeuvVuE2BA==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3962,6 +3993,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/Fabric.Backend.Guild/package.json b/Fabric.Backend.Guild/package.json index 5cb871b..5748ded 100644 --- a/Fabric.Backend.Guild/package.json +++ b/Fabric.Backend.Guild/package.json @@ -16,6 +16,8 @@ "@nestjs/core": "^10.4.8", "@nestjs/platform-express": "^10.4.8", "@nestjs/typeorm": "^11.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", "mysql2": "^3.22.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", diff --git a/Fabric.Backend.Guild/src/main.ts b/Fabric.Backend.Guild/src/main.ts index c86655b..7ab10cc 100644 --- a/Fabric.Backend.Guild/src/main.ts +++ b/Fabric.Backend.Guild/src/main.ts @@ -1,10 +1,18 @@ import 'reflect-metadata'; +import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ); const port = process.env.PORT ? Number(process.env.PORT) : 7002; await app.listen(port); console.log(`Fabric.Backend.Guild listening on :${port}`); diff --git a/Fabric.Backend.Guild/src/messaging/dto.create-message.dto.ts b/Fabric.Backend.Guild/src/messaging/dto.create-message.dto.ts new file mode 100644 index 0000000..3506e51 --- /dev/null +++ b/Fabric.Backend.Guild/src/messaging/dto.create-message.dto.ts @@ -0,0 +1,59 @@ +import { + ArrayMaxSize, + IsArray, + IsOptional, + IsString, + MaxLength, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +class AttachmentDto { + @IsString() + @MaxLength(2048) + url!: string; + + @IsOptional() + @IsString() + @MaxLength(255) + name?: string; + + @IsOptional() + @IsString() + @MaxLength(100) + mimeType?: string; +} + +export class CreateMessageDto { + @IsString() + @MaxLength(4000) + content!: string; + + @IsOptional() + @IsString() + @MaxLength(80) + clientMessageId?: string; + + @IsOptional() + @IsString() + @MaxLength(80) + replyToMessageId?: string; + + @IsOptional() + @IsArray() + @ArrayMaxSize(50) + @IsString({ each: true }) + mentions?: string[]; + + @IsOptional() + @IsArray() + @ArrayMaxSize(10) + @ValidateNested({ each: true }) + @Type(() => AttachmentDto) + attachments?: AttachmentDto[]; + + @IsOptional() + @IsString() + @MaxLength(64) + authorUserId?: string; +} diff --git a/Fabric.Backend.Guild/src/messaging/messaging.controller.ts b/Fabric.Backend.Guild/src/messaging/messaging.controller.ts index 61d9204..f4f442a 100644 --- a/Fabric.Backend.Guild/src/messaging/messaging.controller.ts +++ b/Fabric.Backend.Guild/src/messaging/messaging.controller.ts @@ -1,9 +1,15 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { CreateMessageDto } from './dto.create-message.dto'; type Message = { messageId: string; seq: number; content: string; + authorUserId: string; + replyToMessageId: string | null; + mentions: string[]; + attachments: Array<{ url: string; name?: string; mimeType?: string }>; + createdAt: string; }; @Controller('channels/:id/messages') @@ -12,14 +18,19 @@ export class MessagingController { private messagesByChannel = new Map(); @Post() - create(@Param('id') channelId: string, @Body() body: { content?: string; messageId?: string }) { + create(@Param('id') channelId: string, @Body() body: CreateMessageDto) { const next = (this.seqByChannel.get(channelId) ?? 0) + 1; this.seqByChannel.set(channelId, next); const message: Message = { - messageId: body.messageId ?? `m-${channelId}-${next}`, + messageId: body.clientMessageId ?? `m-${channelId}-${next}`, seq: next, - content: body.content ?? '', + content: body.content, + authorUserId: body.authorUserId ?? 'anonymous', + replyToMessageId: body.replyToMessageId ?? null, + mentions: body.mentions ?? [], + attachments: body.attachments ?? [], + createdAt: new Date().toISOString(), }; const arr = this.messagesByChannel.get(channelId) ?? []; diff --git a/docs/TODO-backend-center-guild.md b/docs/TODO-backend-center-guild.md index ba0278a..e6fc77a 100644 --- a/docs/TODO-backend-center-guild.md +++ b/docs/TODO-backend-center-guild.md @@ -44,7 +44,7 @@ - [x] 索引设计(channel_id + seq, created_at 等) ### 2.2 消息主链路 -- [ ] 发送消息(content/reply/mentions/attachments 元数据) +- [x] 发送消息(content/reply/mentions/attachments 元数据) - [ ] 编辑消息(可编辑窗口策略先简化) - [ ] 删除消息(软删 vs 硬删,先定策略) - [ ] `GET messages` 分页(seq 区间 + limit)