Files
core/src/modules/comment/comment.controller.ts
Innei de0f72f537 chore: update deps
Signed-off-by: Innei <tukon479@gmail.com>
2023-03-29 21:56:10 +08:00

447 lines
12 KiB
TypeScript

import { isUndefined } from 'lodash'
import { Document, FilterQuery } from 'mongoose'
import {
Body,
Delete,
ForbiddenException,
Get,
Param,
Patch,
Post,
Query,
Req,
UseInterceptors,
} from '@nestjs/common'
import { ApiOperation, ApiParam } from '@nestjs/swagger'
import { DocumentType } from '@typegoose/typegoose'
import { ApiController } from '~/common/decorators/api-controller.decorator'
import { Auth } from '~/common/decorators/auth.decorator'
import { CurrentUser } from '~/common/decorators/current-user.decorator'
import { HTTPDecorators } from '~/common/decorators/http.decorator'
import { IpLocation, IpRecord } from '~/common/decorators/ip.decorator'
import { ApiName } from '~/common/decorators/openapi.decorator'
import { IsMaster } from '~/common/decorators/role.decorator'
import { BizException } from '~/common/exceptions/biz.exception'
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'
import { ErrorCodeEnum } from '~/constants/error-code.constant'
import { ReplyMailType } from '~/processors/helper/helper.email.service'
import { EventManagerService } from '~/processors/helper/helper.event.service'
import { MongoIdDto } from '~/shared/dto/id.dto'
import { PagerDto } from '~/shared/dto/pager.dto'
import { transformDataToPaginate } from '~/transformers/paginate.transformer'
import { ConfigsService } from '../configs/configs.service'
import { UserModel } from '../user/user.model'
import {
CommentDto,
CommentRefTypesDto,
CommentStatePatchDto,
TextOnlyDto,
} from './comment.dto'
import { CommentFilterEmailInterceptor } from './comment.interceptor'
import { CommentModel, CommentState } from './comment.model'
import { CommentService } from './comment.service'
const idempotenceMessage = '哦吼,这句话你已经说过啦'
@ApiController({ path: 'comments' })
@UseInterceptors(CommentFilterEmailInterceptor)
@ApiName
export class CommentController {
constructor(
private readonly commentService: CommentService,
private readonly eventManager: EventManagerService,
private readonly configsService: ConfigsService,
) {}
@Get('/')
@Auth()
async getRecentlyComments(@Query() query: PagerDto) {
const { size = 10, page = 1, state = 0 } = query
return transformDataToPaginate(
await this.commentService.getComments({ size, page, state }),
)
}
@Get('/:id')
@ApiOperation({ summary: '根据 comment id 获取评论,包括子评论' })
async getComments(
@Param() params: MongoIdDto,
@IsMaster() isMaster: boolean,
) {
const { id } = params
const data: CommentModel | null = await this.commentService.model
.findOne({
_id: id,
})
.populate('parent')
.lean()
if (!data) {
throw new CannotFindException()
}
if (data.isWhispers && !isMaster) {
throw new CannotFindException()
}
await this.commentService.replaceMasterAvatarUrl([data])
return data
}
// 面向 C 端的评论查询接口
@Get('/ref/:id')
@ApiOperation({ summary: '根据评论的 refId 获取评论,如 Post Id' })
@HTTPDecorators.Paginator
async getCommentsByRefId(
@Param() params: MongoIdDto,
@Query() query: PagerDto,
@IsMaster() isMaster: boolean,
) {
const { id } = params
const { page = 1, size = 10 } = query
const configs = await this.configsService.get('commentOptions')
const { commentShouldAudit } = configs
const $and: FilterQuery<CommentModel & Document<any, any, any>>[] = [
{
parent: undefined,
ref: id,
},
{
$or: commentShouldAudit
? [
{
state: CommentState.Read,
},
]
: [
{
state: CommentState.Read,
},
{ state: CommentState.Unread },
],
},
]
if (isMaster) {
$and.push({
$or: [
{ isWhispers: true },
{ isWhispers: false },
{
isWhispers: { $exists: false },
},
],
})
} else {
$and.push({
$or: [
{ isWhispers: false },
{
isWhispers: { $exists: false },
},
],
})
}
const comments = await this.commentService.model.paginate(
{
$and,
},
{
limit: size,
page,
sort: { pin: -1, created: -1 },
},
)
await this.commentService.replaceMasterAvatarUrl(comments.docs)
return comments
}
@Post('/:id')
@ApiOperation({ summary: '根据文章的 _id 评论' })
@HTTPDecorators.Idempotence({
expired: 20,
errorMessage: idempotenceMessage,
})
async comment(
@Param() params: MongoIdDto,
@Body() body: CommentDto,
@IsMaster() isMaster: boolean,
@IpLocation() ipLocation: IpRecord,
@Query() query: CommentRefTypesDto,
) {
const { disableComment } = await this.configsService.get('commentOptions')
if (disableComment) {
throw new BizException(ErrorCodeEnum.CommentDisabled)
}
if (!isMaster) {
await this.commentService.validAuthorName(body.author)
}
const { ref } = query
const id = params.id
if (!(await this.commentService.allowComment(id, ref)) && !isMaster) {
throw new ForbiddenException('主人禁止了评论')
}
const model: Partial<CommentModel> = { ...body, ...ipLocation }
const comment = await this.commentService.createComment(id, model, ref)
const commentId = comment._id.toString()
process.nextTick(async () => {
if (isMaster) {
return
}
await this.commentService.appendIpLocation(commentId, ipLocation.ip)
})
process.nextTick(async () => {
const configs = await this.configsService.get('commentOptions')
const { commentShouldAudit } = configs
if (await this.commentService.checkSpam(comment)) {
comment.state = CommentState.Junk
await comment.save()
return
} else if (!isMaster) {
this.commentService.sendEmail(comment, ReplyMailType.Owner)
}
if (commentShouldAudit) {
await this.eventManager.broadcast(
BusinessEvents.COMMENT_CREATE,
comment,
{
scope: EventScope.TO_SYSTEM_ADMIN,
},
)
return
}
await this.eventManager.broadcast(
BusinessEvents.COMMENT_CREATE,
comment,
{
scope: isMaster
? EventScope.TO_SYSTEM_VISITOR
: comment.isWhispers
? EventScope.TO_SYSTEM_ADMIN
: EventScope.ALL,
},
)
})
return comment
}
@Post('/reply/:id')
@ApiParam({
name: 'id',
description: 'cid',
example: '5e7370bec56432cbac578e2d',
})
@HTTPDecorators.Idempotence({
expired: 20,
errorMessage: idempotenceMessage,
})
async replyByCid(
@Param() params: MongoIdDto,
@Body() body: CommentDto,
@Body('author') author: string,
@IsMaster() isMaster: boolean,
@IpLocation() ipLocation: IpRecord,
) {
const { disableComment } = await this.configsService.get('commentOptions')
if (disableComment) {
throw new BizException(ErrorCodeEnum.CommentDisabled)
}
if (!isMaster) {
await this.commentService.validAuthorName(author)
}
const { id } = params
const parent = await this.commentService.model.findById(id).populate('ref')
if (!parent) {
throw new CannotFindException()
}
const commentIndex = parent.commentsIndex
const key = `${parent.key}#${commentIndex}`
const model: Partial<CommentModel> = {
parent,
ref: (parent.ref as DocumentType<any>)._id,
refType: parent.refType,
...body,
...ipLocation,
key,
}
const comment = await this.commentService.model.create(model)
const commentId = comment._id.toString()
process.nextTick(async () => {
if (isMaster) {
return
}
await this.commentService.appendIpLocation(commentId, ipLocation.ip)
})
await parent.updateOne({
$push: {
children: comment._id,
},
$inc: {
commentsIndex: 1,
},
state:
comment.state === CommentState.Read &&
parent.state !== CommentState.Read
? CommentState.Read
: parent.state,
})
if (isMaster) {
this.commentService.sendEmail(comment, ReplyMailType.Guest)
this.eventManager.broadcast(BusinessEvents.COMMENT_CREATE, comment, {
scope: EventScope.TO_SYSTEM_VISITOR,
})
} else {
const configs = await this.configsService.get('commentOptions')
const { commentShouldAudit } = configs
if (commentShouldAudit) {
this.eventManager.broadcast(BusinessEvents.COMMENT_CREATE, comment, {
scope: EventScope.TO_SYSTEM_ADMIN,
})
return
}
this.commentService.sendEmail(comment, ReplyMailType.Owner)
this.eventManager.broadcast(BusinessEvents.COMMENT_CREATE, comment, {
scope: EventScope.ALL,
})
}
return comment
}
@Post('/master/comment/:id')
@ApiOperation({ summary: '主人专用评论接口 需要登录' })
@Auth()
@HTTPDecorators.Idempotence({
expired: 20,
errorMessage: idempotenceMessage,
})
async commentByMaster(
@CurrentUser() user: UserModel,
@Param() params: MongoIdDto,
@Body() body: TextOnlyDto,
@IpLocation() ipLocation: IpRecord,
@Query() query: CommentRefTypesDto,
) {
const { name, mail, url } = user
const model: CommentDto = {
author: name,
...body,
mail,
url,
state: CommentState.Read,
} as CommentDto
return await this.comment(params, model as any, true, ipLocation, query)
}
@Post('/master/reply/:id')
@ApiOperation({ summary: '主人专用评论回复 需要登录' })
@ApiParam({ name: 'id', description: 'cid' })
@Auth()
@HTTPDecorators.Idempotence({
expired: 20,
errorMessage: idempotenceMessage,
})
async replyByMaster(
@Req() req: any,
@Param() params: MongoIdDto,
@Body() body: TextOnlyDto,
@IpLocation() ipLocation: IpRecord,
) {
const { name, mail, url } = req.user
const model: CommentDto = {
author: name,
...body,
mail,
url,
state: CommentState.Read,
} as CommentDto
// @ts-ignore
return await this.replyByCid(params, model, undefined, true, ipLocation)
}
@Patch('/:id')
@ApiOperation({ summary: '修改评论的状态' })
@Auth()
async modifyCommentState(
@Param() params: MongoIdDto,
@Body() body: CommentStatePatchDto,
) {
const { id } = params
const { state, pin } = body
const updateResult = {} as any
!isUndefined(state) && Reflect.set(updateResult, 'state', state)
!isUndefined(pin) && Reflect.set(updateResult, 'pin', pin)
if (pin) {
const currentRefModel = await this.commentService.model
.findOne({
_id: id,
})
.lean()
.populate('ref')
const refId = (currentRefModel?.ref as any)?._id
if (refId) {
await this.commentService.model.updateMany(
{
ref: refId,
},
{
pin: false,
},
)
}
}
try {
await this.commentService.model.updateOne(
{
_id: id,
},
updateResult,
)
return
} catch {
throw new NoContentCanBeModifiedException()
}
}
@Delete('/:id')
@Auth()
async deleteComment(@Param() params: MongoIdDto) {
const { id } = params
await this.commentService.deleteComments(id)
await this.eventManager.broadcast(BusinessEvents.COMMENT_DELETE, id, {
scope: EventScope.TO_SYSTEM_VISITOR,
nextTick: true,
})
return
}
}