feat(guild-messaging): support message metadata for reply mentions and attachments

This commit is contained in:
nav
2026-05-12 10:28:02 +00:00
parent ceaece754e
commit d3fdc3dd1e
6 changed files with 124 additions and 4 deletions

View File

@@ -12,6 +12,8 @@
"@nestjs/core": "^10.4.8", "@nestjs/core": "^10.4.8",
"@nestjs/platform-express": "^10.4.8", "@nestjs/platform-express": "^10.4.8",
"@nestjs/typeorm": "^11.0.1", "@nestjs/typeorm": "^11.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
"mysql2": "^3.22.3", "mysql2": "^3.22.3",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
@@ -592,6 +594,12 @@
"undici-types": "~6.21.0" "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": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.59.3", "version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", "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" "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": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -2592,6 +2617,12 @@
"node": ">= 0.8.0" "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": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -3962,6 +3993,15 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "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": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@@ -16,6 +16,8 @@
"@nestjs/core": "^10.4.8", "@nestjs/core": "^10.4.8",
"@nestjs/platform-express": "^10.4.8", "@nestjs/platform-express": "^10.4.8",
"@nestjs/typeorm": "^11.0.1", "@nestjs/typeorm": "^11.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
"mysql2": "^3.22.3", "mysql2": "^3.22.3",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",

View File

@@ -1,10 +1,18 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); app.setGlobalPrefix('api');
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
const port = process.env.PORT ? Number(process.env.PORT) : 7002; const port = process.env.PORT ? Number(process.env.PORT) : 7002;
await app.listen(port); await app.listen(port);
console.log(`Fabric.Backend.Guild listening on :${port}`); console.log(`Fabric.Backend.Guild listening on :${port}`);

View File

@@ -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;
}

View File

@@ -1,9 +1,15 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { CreateMessageDto } from './dto.create-message.dto';
type Message = { type Message = {
messageId: string; messageId: string;
seq: number; seq: number;
content: string; content: string;
authorUserId: string;
replyToMessageId: string | null;
mentions: string[];
attachments: Array<{ url: string; name?: string; mimeType?: string }>;
createdAt: string;
}; };
@Controller('channels/:id/messages') @Controller('channels/:id/messages')
@@ -12,14 +18,19 @@ export class MessagingController {
private messagesByChannel = new Map<string, Message[]>(); private messagesByChannel = new Map<string, Message[]>();
@Post() @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; const next = (this.seqByChannel.get(channelId) ?? 0) + 1;
this.seqByChannel.set(channelId, next); this.seqByChannel.set(channelId, next);
const message: Message = { const message: Message = {
messageId: body.messageId ?? `m-${channelId}-${next}`, messageId: body.clientMessageId ?? `m-${channelId}-${next}`,
seq: 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) ?? []; const arr = this.messagesByChannel.get(channelId) ?? [];

View File

@@ -44,7 +44,7 @@
- [x] 索引设计channel_id + seq, created_at 等) - [x] 索引设计channel_id + seq, created_at 等)
### 2.2 消息主链路 ### 2.2 消息主链路
- [ ] 发送消息content/reply/mentions/attachments 元数据) - [x] 发送消息content/reply/mentions/attachments 元数据)
- [ ] 编辑消息(可编辑窗口策略先简化) - [ ] 编辑消息(可编辑窗口策略先简化)
- [ ] 删除消息(软删 vs 硬删,先定策略) - [ ] 删除消息(软删 vs 硬删,先定策略)
- [ ] `GET messages` 分页seq 区间 + limit - [ ] `GET messages` 分页seq 区间 + limit