From 26b2b4f13451b22d3e242f5ef52b31e4fa95a60a Mon Sep 17 00:00:00 2001 From: Innei Date: Sat, 14 Sep 2024 20:26:18 +0800 Subject: [PATCH] feat: reader for comment and like action (#2122) * init Signed-off-by: Innei * update Signed-off-by: Innei * feat: reader like activity Signed-off-by: Innei * feat: add comment Signed-off-by: Innei * feat: comment reader Signed-off-by: Innei --------- Signed-off-by: Innei --- .../src/common/contexts/request.context.ts | 15 +++--- .../decorators/current-user.decorator.ts | 6 +++ apps/core/src/common/guards/roles.guard.ts | 11 +++++ .../middlewares/request-context.middleware.ts | 6 ++- .../modules/activity/activity.interface.ts | 1 + .../src/modules/activity/activity.service.ts | 39 +++++++++++++++ .../src/modules/comment/comment.controller.ts | 48 +++++++++++++++++-- apps/core/src/modules/comment/comment.dto.ts | 12 +++++ .../core/src/modules/comment/comment.model.ts | 2 + .../src/modules/comment/comment.module.ts | 8 +++- .../src/modules/comment/comment.service.ts | 23 ++++++++- apps/core/src/modules/reader/reader.model.ts | 23 +++++++++ .../core/src/modules/reader/reader.service.ts | 22 +++++---- .../processors/database/database.models.ts | 4 +- .../src/transformers/get-req.transformer.ts | 8 +++- packages/api-client/controllers/comment.ts | 15 +++--- packages/api-client/controllers/index.ts | 19 ++++---- packages/api-client/models/comment.ts | 1 + packages/api-client/models/index.ts | 1 + packages/api-client/models/reader.ts | 9 ++++ 20 files changed, 230 insertions(+), 43 deletions(-) create mode 100644 apps/core/src/modules/reader/reader.model.ts create mode 100644 packages/api-client/models/reader.ts diff --git a/apps/core/src/common/contexts/request.context.ts b/apps/core/src/common/contexts/request.context.ts index 2eb534d3..2b5a2f63 100644 --- a/apps/core/src/common/contexts/request.context.ts +++ b/apps/core/src/common/contexts/request.context.ts @@ -1,18 +1,19 @@ /* eslint-disable dot-notation */ // @reference https://github.com/ever-co/ever-gauzy/blob/d36b4f40b1446f3c33d02e0ba00b53a83109d950/packages/core/src/core/context/request-context.ts import * as cls from 'cls-hooked' -import type { UserDocument } from '~/modules/user/user.model' -import type { IncomingMessage, ServerResponse } from 'node:http' +import type { UserModel } from '~/modules/user/user.model' +import type { BizIncomingMessage } from '~/transformers/get-req.transformer' +import type { ServerResponse } from 'node:http' import { UnauthorizedException } from '@nestjs/common' type Nullable = T | null export class RequestContext { readonly id: number - request: IncomingMessage + request: BizIncomingMessage response: ServerResponse - constructor(request: IncomingMessage, response: ServerResponse) { + constructor(request: BizIncomingMessage, response: ServerResponse) { this.id = Math.random() this.request = request this.response = response @@ -27,7 +28,7 @@ export class RequestContext { return null } - static currentRequest(): Nullable { + static currentRequest(): Nullable { const requestContext = RequestContext.currentRequestContext() if (requestContext) { @@ -37,11 +38,11 @@ export class RequestContext { return null } - static currentUser(throwError?: boolean): Nullable { + static currentUser(throwError?: boolean): Nullable { const requestContext = RequestContext.currentRequestContext() if (requestContext) { - const user: UserDocument = requestContext.request['user'] + const user = requestContext.request['user'] if (user) { return user diff --git a/apps/core/src/common/decorators/current-user.decorator.ts b/apps/core/src/common/decorators/current-user.decorator.ts index 27e1216d..5fcd02b7 100644 --- a/apps/core/src/common/decorators/current-user.decorator.ts +++ b/apps/core/src/common/decorators/current-user.decorator.ts @@ -17,3 +17,9 @@ export const CurrentUserToken = createParamDecorator( return token ? token.replace(/[Bb]earer /, '') : '' }, ) + +export const CurrentReaderId = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + return getNestExecutionContextRequest(ctx).readerId + }, +) diff --git a/apps/core/src/common/guards/roles.guard.ts b/apps/core/src/common/guards/roles.guard.ts index b7f6907c..a61cf4da 100644 --- a/apps/core/src/common/guards/roles.guard.ts +++ b/apps/core/src/common/guards/roles.guard.ts @@ -31,6 +31,17 @@ export class RolesGuard extends AuthGuard implements CanActivate { isAuthenticated = true } catch {} + const session = await this.authService.getSessionUser(request.raw) + + if (session) { + const readerId = session.user.id + request.readerId = readerId + + Object.assign(request.raw, { + readerId, + }) + } + request.isGuest = !isAuthenticated request.isAuthenticated = isAuthenticated diff --git a/apps/core/src/common/middlewares/request-context.middleware.ts b/apps/core/src/common/middlewares/request-context.middleware.ts index ec5aef24..333d71dc 100644 --- a/apps/core/src/common/middlewares/request-context.middleware.ts +++ b/apps/core/src/common/middlewares/request-context.middleware.ts @@ -2,15 +2,17 @@ import * as cls from 'cls-hooked' import type { NestMiddleware } from '@nestjs/common' -import type { IncomingMessage, ServerResponse } from 'node:http' +import type { ServerResponse } from 'node:http' import { Injectable } from '@nestjs/common' +import { BizIncomingMessage } from '~/transformers/get-req.transformer' + import { RequestContext } from '../contexts/request.context' @Injectable() export class RequestContextMiddleware implements NestMiddleware { - use(req: IncomingMessage, res: ServerResponse, next: () => any) { + use(req: BizIncomingMessage, res: ServerResponse, next: () => any) { const requestContext = new RequestContext(req, res) const session = diff --git a/apps/core/src/modules/activity/activity.interface.ts b/apps/core/src/modules/activity/activity.interface.ts index 54e1e828..23be703d 100644 --- a/apps/core/src/modules/activity/activity.interface.ts +++ b/apps/core/src/modules/activity/activity.interface.ts @@ -2,6 +2,7 @@ export interface ActivityLikePayload { id: string ip: string type: ActivityLikeSupportType + readerId?: string } export type ActivityLikeSupportType = 'post' | 'note' diff --git a/apps/core/src/modules/activity/activity.service.ts b/apps/core/src/modules/activity/activity.service.ts index a4c5bde3..99d39882 100644 --- a/apps/core/src/modules/activity/activity.service.ts +++ b/apps/core/src/modules/activity/activity.service.ts @@ -22,6 +22,7 @@ import { Logger, } from '@nestjs/common' +import { RequestContext } from '~/common/contexts/request.context' import { ArticleTypeEnum } from '~/constants/article.constant' import { BusinessEvents, EventScope } from '~/constants/business-event.constant' import { @@ -44,6 +45,7 @@ import { CommentService } from '../comment/comment.service' import { ConfigsService } from '../configs/configs.service' import { NoteService } from '../note/note.service' import { PostService } from '../post/post.service' +import { ReaderModel } from '../reader/reader.model' import { ReaderService } from '../reader/reader.service' import { Activity } from './activity.constant' import { ActivityModel } from './activity.model' @@ -194,6 +196,21 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy { } as Record, ) + const readerIds = [] as string[] + for (const item of activities.docs) { + const readerId = item.payload.readerId + if (readerId) { + readerIds.push(readerId) + } + } + + const readers = await this.readerService.findReaderInIds(readerIds) + + const readerMap = new Map() + for (const reader of readers) { + readerMap.set(reader._id.toHexString(), reader) + } + const type2Collection: Record< ActivityLikeSupportType, Collection @@ -230,6 +247,15 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy { const refModel = refModelData.get(ac.payload.id) refModel && Reflect.set(nextAc, 'ref', refModel) + const readerId = ac.payload.readerId + if (readerId) { + const reader = readerMap.get(readerId) + if (reader) { + Object.assign(nextAc, { + reader, + }) + } + } return nextAc }) as any as (ActivityModel & { @@ -277,11 +303,22 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy { } async likeAndEmit(type: 'post' | 'note', id: string, ip: string) { + const readerId = RequestContext.currentRequest()?.readerId + + let reader: ReaderModel | null = null + if (readerId) { + reader = await this.readerService + .findReaderInIds([readerId]) + .then((res) => res[0]) + } + try { const mapping = { post: ArticleTypeEnum.Post, note: ArticleTypeEnum.Note, } + + // TODO 改成 reader 维度 const res = await this.countingService.updateLikeCountWithIp( mapping[type], id, @@ -302,6 +339,7 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy { { id, type, + reader, ref: pick(refModel, [ 'id', '_id', @@ -325,6 +363,7 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy { ip, type, id, + readerId: reader ? readerId : undefined, } as ActivityLikePayload, }) } diff --git a/apps/core/src/modules/comment/comment.controller.ts b/apps/core/src/modules/comment/comment.controller.ts index d41adf49..094693f2 100644 --- a/apps/core/src/modules/comment/comment.controller.ts +++ b/apps/core/src/modules/comment/comment.controller.ts @@ -1,4 +1,4 @@ -import { isUndefined } from 'lodash' +import { isUndefined, keyBy } from 'lodash' import type { DocumentType } from '@typegoose/typegoose' import type { Document, FilterQuery } from 'mongoose' import type { CommentModel } from './comment.model' @@ -7,7 +7,9 @@ import { Body, Delete, ForbiddenException, + forwardRef, Get, + Inject, Param, Patch, Post, @@ -34,6 +36,8 @@ import { transformDataToPaginate } from '~/transformers/paginate.transformer' import { scheduleManager } from '~/utils/schedule.util' import { ConfigsService } from '../configs/configs.service' +import { ReaderModel } from '../reader/reader.model' +import { ReaderService } from '../reader/reader.service' import { UserModel } from '../user/user.model' import { CommentDto, @@ -55,15 +59,33 @@ export class CommentController { private readonly commentService: CommentService, private readonly eventManager: EventManagerService, private readonly configsService: ConfigsService, + @Inject(forwardRef(() => ReaderService)) + private readonly readerService: ReaderService, ) {} @Get('/') @Auth() async getRecentlyComments(@Query() query: PagerDto) { const { size = 10, page = 1, state = 0 } = query - return transformDataToPaginate( - await this.commentService.getComments({ size, page, state }), + + const comments = await this.commentService.getComments({ + size, + page, + state, + }) + const readers = await this.readerService.findReaderInIds( + comments.docs.map((doc) => doc.readerId).filter(Boolean) as string[], ) + const readerMap = new Map() + for (const reader of readers) { + readerMap.set(reader._id.toHexString(), reader) + } + + const res = transformDataToPaginate(comments) + Object.assign(res, { + readers: keyBy(readers, 'id'), + }) + return res } @Get('/:id') @@ -87,12 +109,18 @@ export class CommentController { } await this.commentService.fillAndReplaceAvatarUrl([data]) + if (data.readerId) { + const reader = await this.readerService.findReaderInIds([data.readerId]) + Object.assign(data, { + reader: reader[0], + }) + } + return data } // 面向 C 端的评论查询接口 @Get('/ref/:id') - @HTTPDecorators.Paginator async getCommentsByRefId( @Param() params: MongoIdDto, @Query() query: PagerDto, @@ -162,7 +190,17 @@ export class CommentController { await this.commentService.fillAndReplaceAvatarUrl(comments.docs) this.commentService.cleanDirtyData(comments.docs) - return comments + const result = transformDataToPaginate(comments) + const readerIds = comments.docs + .map((comment) => comment.readerId) + .filter((id) => !!id) as string[] + const readers = await this.readerService.findReaderInIds(readerIds) + + Object.assign(result, { + readers: keyBy(readers, 'id'), + }) + + return result } @Post('/:id') diff --git a/apps/core/src/modules/comment/comment.dto.ts b/apps/core/src/modules/comment/comment.dto.ts index 065697e2..76b766d8 100644 --- a/apps/core/src/modules/comment/comment.dto.ts +++ b/apps/core/src/modules/comment/comment.dto.ts @@ -55,6 +55,18 @@ export class CommentDto { avatar?: string } +export class RequiredGuestReaderCommentDto extends CommentDto { + @IsString() + @IsNotEmpty() + @MaxLength(20, { message: '昵称不得大于 20 个字符' }) + author: string + + @IsString() + @IsEmail(undefined, { message: '请更正为正确的邮箱' }) + @MaxLength(50, { message: '邮箱地址不得大于 50 个字符' }) + mail: string +} + export class TextOnlyDto { @IsString() @IsNotEmpty() diff --git a/apps/core/src/modules/comment/comment.model.ts b/apps/core/src/modules/comment/comment.model.ts index d731a734..ac4509a6 100644 --- a/apps/core/src/modules/comment/comment.model.ts +++ b/apps/core/src/modules/comment/comment.model.ts @@ -128,4 +128,6 @@ export class CommentModel extends BaseModel { @prop() meta?: string + @prop({}) + readerId?: string } diff --git a/apps/core/src/modules/comment/comment.module.ts b/apps/core/src/modules/comment/comment.module.ts index faeaf947..96dabf09 100644 --- a/apps/core/src/modules/comment/comment.module.ts +++ b/apps/core/src/modules/comment/comment.module.ts @@ -2,6 +2,7 @@ import { forwardRef, Module } from '@nestjs/common' import { GatewayModule } from '~/processors/gateway/gateway.module' +import { ReaderModule } from '../reader/reader.module' import { ServerlessModule } from '../serverless/serverless.module' import { UserModule } from '../user/user.module' import { CommentController } from './comment.controller' @@ -11,6 +12,11 @@ import { CommentService } from './comment.service' controllers: [CommentController], providers: [CommentService], exports: [CommentService], - imports: [UserModule, GatewayModule, forwardRef(() => ServerlessModule)], + imports: [ + UserModule, + GatewayModule, + forwardRef(() => ServerlessModule), + forwardRef(() => ReaderModule), + ], }) export class CommentModule {} diff --git a/apps/core/src/modules/comment/comment.service.ts b/apps/core/src/modules/comment/comment.service.ts index f474adab..5220975b 100644 --- a/apps/core/src/modules/comment/comment.service.ts +++ b/apps/core/src/modules/comment/comment.service.ts @@ -20,6 +20,7 @@ import { } from '@nestjs/common' import { OnEvent } from '@nestjs/event-emitter' +import { RequestContext } from '~/common/contexts/request.context' import { CannotFindException } from '~/common/exceptions/cant-find.exception' import { NoContentCanBeModifiedException } from '~/common/exceptions/no-content-canbe-modified.exception' import { BusinessEvents, EventScope } from '~/constants/business-event.constant' @@ -33,6 +34,8 @@ import { scheduleManager } from '~/utils/schedule.util' import { getAvatar, hasChinese } from '~/utils/tool.util' import { ConfigsService } from '../configs/configs.service' +import { ReaderModel } from '../reader/reader.model' +import { ReaderService } from '../reader/reader.service' import { createMockedContextResponse } from '../serverless/mock-response.util' import { ServerlessService } from '../serverless/serverless.service' import { SnippetType } from '../snippet/snippet.model' @@ -63,6 +66,8 @@ export class CommentService implements OnModuleInit { private readonly serverlessService: ServerlessService, private readonly eventManager: EventManagerService, private readonly barkService: BarkPushService, + @Inject(forwardRef(() => ReaderService)) + private readonly readerService: ReaderService, ) {} private async getMailOwnerProps() { @@ -157,6 +162,20 @@ export class CommentService implements OnModuleInit { doc: Partial, type?: CollectionRefTypes, ) { + const readerId = RequestContext.currentRequest()?.readerId + + let reader: ReaderModel | null = null + if (readerId) { + reader = await this.readerService + .findReaderInIds([readerId]) + .then((readers) => readers[0] ?? null) + } + + if (reader) { + doc.author = reader.name + doc.mail = reader.email + } + let ref: (WriteBaseModel & { _id: any }) | null = null let refType = type if (type) { @@ -181,6 +200,7 @@ export class CommentService implements OnModuleInit { ...doc, state: CommentState.Unread, ref: new Types.ObjectId(id), + readerId: reader ? readerId : undefined, refType, }) @@ -208,6 +228,7 @@ export class CommentService implements OnModuleInit { }) .select('+ip +agent') + const readerId = RequestContext.currentRequest()?.readerId if (!comment) return scheduleManager.schedule(async () => { if (isAuthenticated) { @@ -219,7 +240,7 @@ export class CommentService implements OnModuleInit { scheduleManager.batch(async () => { const configs = await this.configsService.get('commentOptions') const { commentShouldAudit } = configs - if (await this.checkSpam(comment)) { + if ((await this.checkSpam(comment)) && !readerId) { await this.commentModel.updateOne( { _id: commentId }, { diff --git a/apps/core/src/modules/reader/reader.model.ts b/apps/core/src/modules/reader/reader.model.ts new file mode 100644 index 00000000..e2fd81f4 --- /dev/null +++ b/apps/core/src/modules/reader/reader.model.ts @@ -0,0 +1,23 @@ +import { modelOptions, prop } from '@typegoose/typegoose' + +import { BaseModel } from '~/shared/model/base.model' + +@modelOptions({ + options: { + customName: 'readers', + }, +}) +export class ReaderModel extends BaseModel { + @prop() + email: string + @prop() + name: string + + @prop() + handle: string + @prop() + image: string + + @prop() + isOwner: boolean +} diff --git a/apps/core/src/modules/reader/reader.service.ts b/apps/core/src/modules/reader/reader.service.ts index 1890f6ef..45b28f3e 100644 --- a/apps/core/src/modules/reader/reader.service.ts +++ b/apps/core/src/modules/reader/reader.service.ts @@ -2,14 +2,21 @@ import { Document } from 'mongodb' import { Types } from 'mongoose' import { Injectable } from '@nestjs/common' +import { ReturnModelType } from '@typegoose/typegoose' import { DatabaseService } from '~/processors/database/database.service' +import { InjectModel } from '~/transformers/model.transformer' import { AUTH_JS_USER_COLLECTION } from '../auth/auth.constant' +import { ReaderModel } from './reader.model' @Injectable() export class ReaderService { - constructor(private readonly databaseService: DatabaseService) {} + constructor( + private readonly databaseService: DatabaseService, + @InjectModel(ReaderModel) + private readonly readerModel: ReturnModelType, + ) {} private buildQueryPipeline(where?: Record): Document[] { const basePipeline: Document[] = [ @@ -81,13 +88,10 @@ export class ReaderService { .updateOne({ _id: new Types.ObjectId(id) }, { $set: { isOwner: false } }) } async findReaderInIds(ids: string[]) { - return this.databaseService.db - .collection(AUTH_JS_USER_COLLECTION) - .aggregate( - this.buildQueryPipeline({ - _id: { $in: ids.map((id) => new Types.ObjectId(id)) }, - }), - ) - .toArray() + return this.readerModel + .find({ + _id: { $in: ids.map((id) => new Types.ObjectId(id)) }, + }) + .lean() } } diff --git a/apps/core/src/processors/database/database.models.ts b/apps/core/src/processors/database/database.models.ts index 3a3d6012..ba02edaf 100644 --- a/apps/core/src/processors/database/database.models.ts +++ b/apps/core/src/processors/database/database.models.ts @@ -10,6 +10,7 @@ import { NoteModel } from '~/modules/note/note.model' import { PageModel } from '~/modules/page/page.model' import { PostModel } from '~/modules/post/post.model' import { ProjectModel } from '~/modules/project/project.model' +import { ReaderModel } from '~/modules/reader/reader.model' import { RecentlyModel } from '~/modules/recently/recently.model' import { SayModel } from '~/modules/say/say.model' import { ServerlessStorageModel } from '~/modules/serverless/serverless.model' @@ -24,8 +25,8 @@ import { WebhookModel } from '~/modules/webhook/webhook.model' import { getProviderByTypegooseClass } from '~/transformers/model.transformer' export const databaseModels = [ - AISummaryModel, ActivityModel, + AISummaryModel, AnalyzeModel, AuthnModel, CategoryModel, @@ -36,6 +37,7 @@ export const databaseModels = [ PageModel, PostModel, ProjectModel, + ReaderModel, RecentlyModel, SayModel, ServerlessStorageModel, diff --git a/apps/core/src/transformers/get-req.transformer.ts b/apps/core/src/transformers/get-req.transformer.ts index d56b7747..3a27d226 100644 --- a/apps/core/src/transformers/get-req.transformer.ts +++ b/apps/core/src/transformers/get-req.transformer.ts @@ -1,14 +1,20 @@ import type { ExecutionContext } from '@nestjs/common' import type { UserModel } from '~/modules/user/user.model' import type { FastifyRequest } from 'fastify' +import type { IncomingMessage } from 'node:http' -export type FastifyBizRequest = FastifyRequest & { +type BizRequest = { user?: UserModel isGuest: boolean isAuthenticated: boolean token?: string + readerId?: string } + +export type FastifyBizRequest = FastifyRequest & BizRequest + +export type BizIncomingMessage = IncomingMessage & BizRequest export function getNestExecutionContextRequest( context: ExecutionContext, ): FastifyBizRequest { diff --git a/packages/api-client/controllers/comment.ts b/packages/api-client/controllers/comment.ts index 55270c78..cee4fadb 100644 --- a/packages/api-client/controllers/comment.ts +++ b/packages/api-client/controllers/comment.ts @@ -2,6 +2,7 @@ import type { IRequestAdapter } from '~/interfaces/adapter' import type { IController } from '~/interfaces/controller' import type { PaginationParams } from '~/interfaces/params' import type { IRequestHandler } from '~/interfaces/request' +import type { ReaderModel } from '~/models' import type { PaginateResult } from '~/models/base' import type { CommentModel } from '~/models/comment' import type { HTTPClient } from '../core' @@ -31,7 +32,7 @@ export class CommentController implements IController { } /** - * 根据 comment id 获取评论, 包括子评论 + * 根据 comment id 获取评论,包括子评论 */ getById(id: string) { return this.proxy(id).get() @@ -43,11 +44,13 @@ export class CommentController implements IController { */ getByRefId(refId: string, pagination: PaginationParams = {}) { const { page, size } = pagination - return this.proxy - .ref(refId) - .get>({ - params: { page: page || 1, size: size || 10 }, - }) + return this.proxy.ref(refId).get< + PaginateResult & { + readers: Record + } + >({ + params: { page: page || 1, size: size || 10 }, + }) } /** * 评论 diff --git a/packages/api-client/controllers/index.ts b/packages/api-client/controllers/index.ts index 804676f4..52b596c0 100644 --- a/packages/api-client/controllers/index.ts +++ b/packages/api-client/controllers/index.ts @@ -72,10 +72,10 @@ export const allControllerNames = [ ] as const export { - AIController, AckController, ActivityController, AggregateController, + AIController, CategoryController, CommentController, LinkController, @@ -83,16 +83,15 @@ export { PageController, PostController, ProjectController, - RecentlyController, - SayController, - SearchController, - SnippetController, - ServerlessController, - SubscribeController, - UserController, - TopicController, - // Enum RecentlyAttitudeEnum, RecentlyAttitudeResultEnum, + RecentlyController, + SayController, + SearchController, + ServerlessController, + SnippetController, + SubscribeController, + TopicController, + UserController, } diff --git a/packages/api-client/models/comment.ts b/packages/api-client/models/comment.ts index 6a089aa3..e6095bbf 100644 --- a/packages/api-client/models/comment.ts +++ b/packages/api-client/models/comment.ts @@ -27,6 +27,7 @@ export interface CommentModel extends BaseModel { location?: string source?: string + readerId?: string } export interface CommentRef { id: string diff --git a/packages/api-client/models/index.ts b/packages/api-client/models/index.ts index bb968436..4dfa1e4e 100644 --- a/packages/api-client/models/index.ts +++ b/packages/api-client/models/index.ts @@ -10,6 +10,7 @@ export * from './note' export * from './page' export * from './post' export * from './project' +export * from './reader' export * from './recently' export * from './say' export * from './setting' diff --git a/packages/api-client/models/reader.ts b/packages/api-client/models/reader.ts new file mode 100644 index 00000000..aa947a31 --- /dev/null +++ b/packages/api-client/models/reader.ts @@ -0,0 +1,9 @@ +export interface ReaderModel { + email: string + name: string + handle: string + + image: string + + isOwner: boolean +}